<!DOCTYPE html>
<html lang="zh-CN">

<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="虚拟人助理" />

<head>
    <meta charset="utf-8">
    <title>万事通</title>
    <style>
        * {
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji" !important;
        }

        html {
            width: 100%;
            height: 100%;
        }

        body {
            background-color: #edeff2;
            width: 100%;
            height: 100%;
            margin: 0;
        }

        .chat_window {
            position: absolute;
            width: 100%;
            max-width: 1100px;
            height: 100%;
            max-height: 888px;
            border-radius: 8px;
            background-color: #fff;
            left: 50%;
            top: 50%;
            overflow: hidden;
            transform: translate(-50%, -50%);
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
        }

        .top_menu {
            background-color: #f6f6f6;
            width: 100%;
            height: 50px;
            padding: 12px 0;
        }

        .top_menu .toggler {
            position: absolute;
            left: 15px;
            top: 5px;
            width: 40px;
            height: 40px;
            padding: 5px 7px;
            border-radius: 4px;
            cursor: pointer;
        }

        .top_menu .toggler:hover {
            background: #e7e7e8;
        }

        .top_menu .toggler .button {
            width: 26px;
            height: 4px;
            border-radius: 4px;
            position: absolute;
            pointer-events: none;
        }

        .top_menu .toggler .button.close {
            top: 8px;
            background-color: #99c959;
        }

        .top_menu .toggler .button.minimize {
            top: 18px;
            background-color: #f8b26a;
        }

        .top_menu .toggler .button.maximize {
            top: 28px;
            background-color: #e15b64;
        }

        .top_menu .title {
            text-align: center;
            color: #909090;
            font-size: 20px;
            line-height: 26px;
        }

        .messages {
            position: relative;
            height: calc(100% - 117px);
            overflow-x: hidden;
            overflow-y: auto;
        }

        .messages::-webkit-scrollbar,
        #chatlog .markdown-body>pre::-webkit-scrollbar,
        #setDialog::-webkit-scrollbar,
        #chatList::-webkit-scrollbar {
            width: 10px;
            height: 10px;
        }

        .messages::-webkit-scrollbar-track,
        #chatlog .response>pre::-webkit-scrollbar-track,
        #setDialog::-webkit-scrollbar-track,
        #chatList::-webkit-scrollbar-track {
            background-clip: padding-box;
            background: transparent;
            border: none;
        }

        .messages::-webkit-scrollbar-corner,
        #chatlog .response>pre::-webkit-scrollbar-corner,
        #setDialog::-webkit-scrollbar-corner,
        #chatList::-webkit-scrollbar-corner {
            background-color: transparent;
        }

        .messages::-webkit-scrollbar-thumb,
        #chatlog .response>pre::-webkit-scrollbar-thumb,
        #setDialog::-webkit-scrollbar-thumb,
        #chatList::-webkit-scrollbar-thumb {
            background-color: rgba(0, 0, 0, 0.1);
            background-clip: padding-box;
            border: solid transparent;
            border-radius: 10px;
        }

        .messages::-webkit-scrollbar-thumb:hover,
        #chatlog .response>pre::-webkit-scrollbar-thumb:hover,
        #setDialog::-webkit-scrollbar-thumb:hover,
        #chatList::-webkit-scrollbar-thumb:hover {
            background-color: rgba(0, 0, 0, 0.4);
        }

        .messages .message {
            clear: both;
            overflow: hidden;
            margin-bottom: 20px;
            transition: all 0.5s linear;
            opacity: 0;
        }

        .messages .message.left .avatar {
            background-color: #f5886e;
            float: left;
        }

        .messages .message.left .text {
            color: #c48843;
        }

        .messages .message.right .avatar {
            background-color: #fdbf68;
            float: right;
        }

        .messages .message.right .text {
            color: #45829b;
        }

        .messages .message.appeared {
            opacity: 1;
        }

        .messages .message .avatar {
            width: 60px;
            height: 60px;
            border-radius: 50%;
            display: inline-block;
        }

        .messages {
            font-size: 16px;
            color: #343541;
            text-align: center;
        }

        #chatlog {
            word-wrap: break-word;
            text-align: start;
        }

        #chatlog>div {
            padding: 18px 25px;
        }

        #chatlog .request {
            position: relative;
        }

        .requestBody {
            white-space: pre-wrap;
        }

        #chatlog .response {
            background: #f7f7f8;
            position: relative;
        }

        .response .markdown-body {
            background: #f7f7f8 !important;
        }

        #chatlog .markdown-body>pre {
            overflow-x: auto;
            padding: 10px;
            position: relative;
            background: rgba(180, 180, 180, 0.1);
        }

        .m-mdic-copy-wrapper {
            position: absolute;
            top: 5px;
            right: 16px;
        }

        .m-mdic-copy-wrapper span.u-mdic-copy-code_lang {
            position: absolute;
            top: 3px;
            right: calc(100% + 4px);
            font-family: system-ui;
            font-size: 12px;
            line-height: 18px;
            color: #555;
            opacity: 0.3;
        }

        .m-mdic-copy-wrapper div.u-mdic-copy-notify {
            position: absolute;
            top: 0;
            right: 0;
            padding: 3px 6px;
            border: 0;
            border-radius: 3px;
            background: none;
            font-family: system-ui;
            font-size: 12px;
            line-height: 18px;
            color: #555;
            opacity: 0.3;
            outline: none;
            opacity: 1;
            right: 100%;
            padding-right: 4px;
        }

        .m-mdic-copy-wrapper button.u-mdic-copy-btn {
            position: relative;
            top: 0;
            right: 0;
            padding: 3px 6px;
            border: 0;
            border-radius: 3px;
            background: none;
            font-family: system-ui;
            font-size: 12px;
            line-height: 18px;
            color: #555;
            opacity: 0.3;
            outline: none;
            cursor: pointer;
            transition: opacity 0.2s;
        }

        .m-mdic-copy-wrapper button.u-mdic-copy-btn:hover {
            opacity: 1;
        }

        #stopChat {
            display: none;
            margin: 0 auto;
            margin-top: 3px;
            width: 80px;
            height: 32px;
            text-align: center;
            line-height: 32px;
            color: white;
            background: #f8b26a;
            cursor: pointer;
            border-radius: 3px;
            position: sticky;
            bottom: 2px;
            justify-content: center;
            align-items: center;
        }

        #stopChat>svg {
            margin-right: 8px;
        }

        #stopChat:hover {
            background: #f0aa60;
        }

        .bottom_wrapper {
            position: relative;
            width: 100%;
            background-color: #fff;
            padding: 10px 10px;
            position: absolute;
            bottom: 0;
        }

        .bottom_wrapper .message_input_wrapper {
            border: none;
            width: calc(100% - 143px);
            position: relative;
            text-align: left;
        }

        .bottom_wrapper .message_input_wrapper .message_input_text {
            border-radius: 4px;
            border: none;
            outline: none;
            resize: none;
            background: #f6f6f6;
            color: #24292f;
            height: 47px;
            font-size: 16px;
            max-height: 200px;
            padding: 13px 0 13px 16px;
            width: 100%;
            display: block;
            transition: background 0.3s;
        }

        .bottom_wrapper .message_input_wrapper .message_input_text:focus {
            background: #f0f0f0;
        }

        .bottom_wrapper .message_input_wrapper .message_input_text::-webkit-scrollbar {
            display: none;
            width: 0;
            height: 0;
        }

        #sendbutton {
            width: 80px;
            height: 47px;
            font-size: 18px;
            font-weight: bold;
            border-radius: 3px;
            background-color: #b8da8b;
            border: none;
            padding: 0;
            color: #fff;
            cursor: pointer;
            transition: all 0.2s linear;
            text-align: center;
            float: right;
            position: absolute;
            right: 65px;
            bottom: 10px;
            cursor: not-allowed;
        }

        .activeSendBtn {
            background-color: #99c959 !important;
            cursor: pointer !important;
        }

        .activeSendBtn:hover {
            background-color: #90c050 !important;
        }

        #clearConv {
            position: absolute;
            right: 10px;
            bottom: 10px;
            width: 47px;
            height: 47px;
            font-size: 16px;
            display: inline-block;
            border-radius: 3px;
            background-color: #909090;
            border: 2px solid #909090;
            color: #fff;
            cursor: pointer;
            transition: all 0.2s linear;
        }

        #clearConv>svg {
            display: block;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        #clearConv:hover {
            background-color: gray;
            border: 2px solid gray;
        }

        .loaded>span {
            display: inline-block;
        }

        .loaded>svg {
            display: none;
        }

        .loading {
            background: #e7e7e8 !important;
        }

        .loading>span {
            display: none;
        }

        .loading>svg {
            display: block;
        }

        .switch-slide {
            display: inline-block;
            vertical-align: middle;
        }

        .switch-slide-label {
            display: block;
            width: 38px;
            height: 18px;
            background: #909090;
            border-radius: 30px;
            cursor: pointer;
            position: relative;
            -webkit-transition: 0.3s ease;
            transition: 0.3s ease;
        }

        .switch-slide-label:after {
            content: "";
            display: block;
            width: 16px;
            height: 16px;
            border-radius: 100%;
            background: #fff;
            box-shadow: 0 1px 1px rgba(0, 0, 0, .1);
            position: absolute;
            left: 1px;
            top: 1px;
            -webkit-transform: translateZ(0);
            transform: translateZ(0);
            -webkit-transition: 0.3s ease;
            transition: 0.3s ease;
        }

        .switch-slide input:checked+label {
            background: #99c959;
            transition: 0.3s ease;
        }

        .switch-slide input:checked+label:after {
            left: 21px;
        }

        #setting {
            position: absolute;
            right: 15px;
            top: 5px;
            cursor: pointer;
            padding: 5px;
            border: none;
            background-color: transparent;
            border-radius: 4px;
        }

        #setting:hover {
            background: #e7e7e8;
        }

        .showSetting {
            background: #b0b0b0 !important;
        }

        #setDialog {
            color: #303030;
            position: absolute;
            z-index: 2;
            background: #f6f6f6;
            width: 320px;
            right: 6px;
            top: 46px;
            overflow-y: auto;
            max-height: calc(100% - 55px);
            -webkit-user-select: none;
            user-select: none;
            border-radius: 5px;
            padding: 8px 12px 8px 12px;
            box-shadow: 0 0 6px rgba(0, 0, 0, 0.15);
        }

        #setDialog input {
            width: 100%;
        }

        #setDialog .inlineTitle {
            display: inline-block;
            width: 80px;
            line-height: 16px;
            vertical-align: middle;
        }

        #convOption,
        #speechOption,
        #speechDetail,
        #recOption {
            margin-bottom: 6px;
        }

        #convOption>div,
        #speechOption>div,
        #speechDetail>div,
        #recOption div {
            margin-top: 10px;
        }

        .inputTextClass {
            outline: none;
            border-radius: 2px;
            margin-top: 2px;
            height: 32px;
            font-size: 15px;
            padding-left: 6px;
            background: white;
            border: none;
        }

        .areaTextClass {
            width: 100%;
            height: 80px;
            display: block;
            resize: none;
            padding: 6px;
        }

        input[type="range"] {
            -webkit-appearance: none;
            display: block;
            margin: 6px 0 3px 0;
            height: 8px;
            background: #909090;
            border-radius: 5px;
            background-image: linear-gradient(#99c959, #99c959);
            background-size: 100% 100%;
            background-repeat: no-repeat;
        }

        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            height: 15px;
            width: 15px;
            border-radius: 50%;
            background: #99c959;
            cursor: ew-resize;
            box-shadow: 0 0 2px 0 #555;
        }

        input[type=range]::-webkit-slider-runnable-track {
            -webkit-appearance: none;
            box-shadow: none;
            border: none;
            background: transparent;
        }

        .justSetLine {
            display: flex;
            justify-content: space-between;
        }

        .presetSelect>div {
            display: inline-block;
        }

        .presetSelect select {
            outline: none;
            border-radius: 3px;
            width: 120px;
            border-color: rgba(0, 0, 0, .3);
        }

        .selectDef {
            display: flex;
            justify-content: space-between;
            font-size: 13px;
            color: #707070;
        }

        #preSetSpeech {
            width: 100%;
            outline: none;
            height: 30px;
            font-size: 14px;
            margin-top: 5px;
            border-radius: 3px;
            border-color: rgba(0, 0, 0, .3);
        }

        .mdOption {
            position: absolute;
            right: 5px;
            bottom: 0px;
            pointer-events: none;
        }

        .mdOption>div {
            margin-top: 4px;
            pointer-events: auto;
            cursor: pointer;
        }

        .mdOption>div>svg {
            color: #e3e3e3;
            display: block;
        }

        .mdOption svg:hover {
            color: #989898;
        }

        .mdOption svg * {
            pointer-events: none;
        }

        .refreshReq svg:not(:first-child) {
            display: none;
        }

        .halfRefReq svg:not(:nth-child(2)) {
            display: none;
        }

        .moreOption {
            position: relative;
        }

        .optionItems {
            position: absolute;
            top: -8px;
            display: flex;
            justify-content: space-between;
            visibility: hidden;
            z-index: 1;
            color: #808080;
        }

        .moreOptionHidden>div {
            display: none !important;
        }

        .optionItems:hover {
            visibility: visible;
        }

        .optionItems:hover .optionItem {
            transform: scale(1);
            visibility: visible;
        }

        .optionTrigger:hover+.optionItems .optionItem:nth-of-type(3) {
            transform: scale(1);
            visibility: visible;
            transition-delay: 50ms;
            transition-duration: 60ms;
        }

        .optionTrigger:hover+.optionItems .optionItem:nth-of-type(2) {
            visibility: visible;
            transition-delay: 80ms;
            transform: scale(1);
            transition-duration: 60ms;
        }

        .optionTrigger:hover+.optionItems .optionItem:nth-of-type(1) {
            visibility: visible;
            transition-delay: 110ms;
            transform: scale(1);
            transition-duration: 60ms;
        }

        .optionItem {
            border-radius: 3px;
            height: 30px;
            width: 30px;
            background-color: #f7f7f8;
            visibility: hidden;
            transform: scale(0);
            display: flex !important;
            justify-content: center;
            align-items: center;
        }

        .optionItem * {
            pointer-events: none;
        }

        .optionItem:hover {
            background: #e0e0e0;
        }

        .readyVoice svg:not(:first-child) {
            display: none;
        }

        .pauseVoice svg:not(:nth-child(2)) {
            display: none;
        }

        .resumeVoice svg:not(:nth-child(3)) {
            display: none;
        }

        #voiceTypes>span {
            border-radius: 3px;
            margin-left: 4px;
            cursor: pointer;
            padding: 1px 5px;
        }

        #voiceTypes>span:hover {
            background: #e7e7e7;
        }

        .selVoiceType {
            background: #d0d0d0 !important;
        }

        .overlay {
            width: 100%;
            height: 100%;
            position: absolute;
            left: 0;
            top: 0;
            background-color: rgba(0, 0, 0, .3);
            z-index: 90;
            opacity: 0;
            visibility: hidden;
            transition: all 200ms ease-in;
        }

        .nav {
            width: 250px;
            height: 100%;
            background-color: #fafafa;
            left: -250px;
            top: 0;
            position: fixed;
            transition: left 100ms ease-in-out;
            z-index: 99;
            display: flex;
            flex-direction: column;
        }

        #newChat {
            text-align: center;
            width: 100%;
            height: 50px;
            color: #404040;
            margin: 10px 0 10px 0;
            display: flex;
            align-items: center;
            cursor: pointer;
        }

        #newChat:hover {
            background: #f0f0f0 !important;
        }

        #newChat>svg {
            margin-left: 17px;
            margin-right: 14px;
        }

        #newChat>span {
            height: 20px;
            line-height: 20px;
        }

        .divider {
            width: 100%;
            border-top: 1px solid #e0e0e0;
            margin: 8px 0;
        }

        .navFooter {
            padding-bottom: 8px;
        }

        .navFunc {
            display: flex;
            justify-content: space-around;
        }

        .navFunc>div,
        .navFunc>label {
            border: 1px solid #e0e0e0;
            border-radius: 3px;
            text-align: center;
            padding: 5px;
            width: 70px;
            color: #404040;
            font-size: 14px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .navFunc>div>svg,
        .navFunc>label>svg {
            color: #666;
        }

        .navFunc>div:hover,
        .navFunc>label:hover {
            background: #e0e0e0;
        }

        .links {
            text-align: center;
        }

        .links a {
            color: #404040;
            text-decoration: none;
        }

        .links a:hover {
            color: black !important;
        }

        .links a:visited {
            color: #404040;
        }

        #chatList {
            width: 100%;
            flex: 1;
            overflow-y: auto;
        }

        .chatLi {
            cursor: pointer;
            width: 100%;
            height: 50px;
            color: #909090;
            display: flex;
            justify-content: space-between;
            align-items: center;
            position: relative;
        }

        .chatLi * {
            pointer-events: none;
        }

        .chatLi>svg {
            margin-left: 18px;
            color: #505050;
        }

        .chatLi svg {
            pointer-events: auto;
        }

        .chatLi svg * {
            pointer-events: none;
        }

        .chatLi .chatName {
            position: absolute;
            left: 55px;
            color: #404040;
            text-overflow: ellipsis;
            white-space: nowrap;
            overflow: hidden;
            max-width: 125px;
            line-height: 20px;
            height: 20px;
        }

        .chatLi .chatOption {
            visibility: hidden;
            display: flex;
            color: #666;
            margin-right: 8px;
        }

        .chatLi:hover {
            background: #f0f0f0;
        }

        .activeChatLi {
            background: #e0e0e0 !important;
        }

        .activeChatLi .chatOption {
            visibility: visible !important;
        }

        .activeChatLi #activeChatEdit {
            position: absolute;
            left: 52px;
            font-size: 16px;
            border-radius: 2px;
            background: #f0f0f0;
            outline: none;
            border: none;
            pointer-events: auto;
            height: 24px;
            line-height: 24px;
            width: 160px;
            padding: 1px 3px;
            z-index: 1;
        }

        .show-nav .nav {
            left: 0;
            box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 10px -5px, rgba(0, 0, 0, 0.14) 0px 16px 24px 2px, rgba(0, 0, 0, 0.12) 0px 6px 30px 5px;
        }

        .show-nav .overlay {
            opacity: 1;
            visibility: visible;
        }

        #loadMask {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            z-index: 100;
            background-color: #edeff2;
        }

        #loadMask>div {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            text-align: center;
        }

        #loadMask svg {
            width: 160px;
            height: 80px;
        }

        #loadMask>div>div {
            font-size: 40px;
            color: #909090;
        }

        #voiceRec {
            position: absolute;
            right: 0;
            top: 0;
            width: 47px;
            height: 100%;
        }

        .message_if_voice {
            padding-right: 47px !important;
        }

        #voiceRecIcon {
            width: 100%;
            height: 100%;
            text-align: center;
            cursor: pointer;
            position: relative;
        }

        #voiceRecIcon:hover>svg {
            color: #808080;
        }

        #voiceRecIcon>svg {
            width: 28px;
            height: 28px;
            color: #b0b0b0;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        #voiceRecIcon>svg .animVoice {
            display: none;
        }

        .voiceRecing>svg {
            color: #99c959 !important;
        }

        .voiceRecing .animVoice {
            display: inline !important;
            transform-origin: 0 64%;
            animation-duration: 1.5s;
            animation-name: scaleVoice;
            animation-timing-function: ease;
            animation-iteration-count: infinite;
        }

        .voiceLong .animVoice {
            display: inline !important;
            transform-origin: 0 64%;
            animation-duration: 0.3s;
            animation-name: longVoice;
            animation-timing-function: ease-in-out;
            animation-iteration-count: 1;
        }

        @keyframes longVoice {
            0% {
                transform: scaleY(0);
            }

            100% {
                transform: scaleY(1);
            }
        }

        @keyframes scaleVoice {
            0% {
                transform: scaleY(0.28);
            }

            20% {
                transform: scaleY(0.60);
            }

            28% {
                transform: scaleY(0.28);
            }

            36% {
                transform: scaleY(0.45);
            }

            44% {
                transform: scaleY(0.28);
            }

            52% {
                transform: scaleY(0.45);
            }

            62% {
                transform: scaleY(0.80);
            }

            72% {
                transform: scaleY(0.80);
            }

            90% {
                transform: scaleY(0.28);
            }

            100% {
                transform: scaleY(0.28);
            }
        }

        #voiceRecSetting {
            display: none;
            position: absolute;
            top: -70px;
            left: -26px;
            z-index: 1;
            padding: 4px 4px;
            user-select: none;
            border-radius: 3px;
            background-color: #f6f6f6;
            box-shadow: 0 0 6px rgba(0, 0, 0, 0.15);
        }

        #voiceRecSetting select {
            width: 102px;
            outline: none;
            height: 28px;
            border-radius: 3px;
            border-color: rgba(0, 0, 0, .3);
        }

        .presetModelCls label {
            margin-right: 8px;
        }

        .presetModelCls select {
            height: 30px;
            margin-top: 2px;
            font-size: 15px;
        }

        .setSwitch {
            display: flex;

        }

        .setSwitch>div {
            border-radius: 3px;
            width: calc(100% / 3);
            height: 32px;
            line-height: 32px;
            text-align: center;
            cursor: pointer;
        }

        .setSwitch>div:hover {
            background-color: #e0e0e0;
        }

        .activeSwitch {
            background-color: #d0d0d0 !important;
        }

        #checkVoiceLoad {
            height: 32px;
            border-radius: 3px;
            line-height: 32px;
            background: #d0d0d0;
            text-align: center;
            display: flex;
            justify-content: center;
            cursor: pointer;
        }

        #checkVoiceLoad:hover {
            background: #c0c0c0;
        }

        .voiceChecking {
            background-color: #c0c0c0 !important;
        }

        .voiceChecking>svg {
            display: inline !important;
        }

        #checkVoiceLoad>svg {
            display: none;
            margin-right: 8px;
            height: 32px;
            width: 64px;
        }

        #preSetSystem {
            height: 20px;
            line-height: 20px;
            vertical-align: top;
        }
    </style>
</head>

<body>
    <div style="display: none">
        <svg>
            <symbol viewBox="0 0 24 24" id="optionIcon">
                <path fill="currentColor"
                    d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2zm0 14c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2zm0-7c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2z">
                </path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="refreshIcon">
                <path fill="currentColor"
                    d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z">
                </path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="halfRefIcon">
                <path fill="currentColor"
                    d="M 4.009 12.163 C 4.012 12.206 2.02 12.329 2 12.098 C 2 6.575 6.477 2 12 2 C 17.523 2 22 6.477 22 12 C 22 14.136 21.33 16.116 20.19 17.74 L 17 12 L 20 12 C 19.999 5.842 13.333 1.993 7.999 5.073 C 3.211 8.343 4.374 12.389 4.009 12.163 Z" />
            </symbol>
            <symbol viewBox="-2 -2 20 20" id="copyIcon">
                <path fill="currentColor"
                    d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z">
                </path>
                <path fill="currentColor"
                    d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z">
                </path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="delIcon">
                <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                    d="M9 7v0a3 3 0 0 1 3-3v0a3 3 0 0 1 3 3v0M9 7h6M9 7H6m9 0h3m2 0h-2M4 7h2m0 0v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7">
                </path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="readyVoiceIcon">
                <path fill="currentColor"
                    d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z">
                </path>
            </symbol>
            <symbol viewBox="0 0 16 16" id="pauseVoiceIcon">
                <path stroke="currentColor" stroke-width="2" d="M5 2v12M11 2v12"></path>
            </symbol>
            <symbol viewBox="0 0 16 16" id="resumeVoiceIcon">
                <path fill="currentColor" d="M4 2L4 14L12 8Z"></path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="stopResIcon">
                <path fill="currentColor"
                    d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10zm0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16zM9 9h6v6H9V9z">
                </path>
            </symbol>
            <symbol viewBox="0 0 128 128" id="downAudioIcon">
                <path
                    d="M 64.662 1.549 C 56.549 4.524, 46.998 14.179, 45.523 20.895 C 45.041 23.089, 44.073 23.833, 40.433 24.807 C 34.752 26.326, 27.956 32.929, 25.527 39.289 C 24.273 42.574, 23.884 45.715, 24.196 50.034 C 24.620 55.897, 24.528 56.193, 21.836 57.585 C 17.142 60.012, 16 63.617, 16 76 C 16 88.463, 17.137 91.985, 21.967 94.483 C 28.244 97.729, 36.120 95.350, 38.579 89.466 C 39.387 87.532, 40 82.764, 40 78.415 C 40 70.971, 40.060 70.783, 42.250 71.370 C 43.487 71.701, 48.888 71.979, 54.250 71.986 L 64 72 64 76 L 64 80 57.122 80 C 49.420 80, 48.614 80.543, 47.547 86.453 C 46.552 91.964, 43.550 97.473, 40.273 99.803 C 33 104.974, 23.120 105.042, 16.118 99.971 C 11.407 96.558, 9.048 92.484, 8.145 86.205 C 6.963 77.979, 0.794 77.729, 0.191 85.883 C -0.196 91.111, 3.323 99.170, 8.062 103.908 C 11.290 107.136, 20.073 111.969, 22.750 111.990 C 23.540 111.996, 24 113.472, 24 116 C 24 119.740, 23.813 120, 21.122 120 C 17.674 120, 15.727 122.044, 16.173 125.195 C 16.492 127.441, 16.781 127.500, 27.391 127.500 C 36.676 127.500, 38.445 127.242, 39.386 125.750 C 40.993 123.203, 38.986 120.568, 35.149 120.187 C 32.206 119.894, 32 119.617, 32 115.956 C 32 112.509, 32.330 111.959, 34.750 111.377 C 42.181 109.591, 52.157 101.208, 53.575 95.559 C 53.928 94.152, 54.514 93, 54.878 93 C 55.242 93, 59.797 97.275, 65 102.500 C 70.762 108.286, 75.256 112, 76.495 112 C 77.769 112, 83.287 107.231, 91.264 99.236 C 101.113 89.366, 104 85.876, 104 83.843 C 104 80.580, 102.553 80, 94.418 80 L 88 80 88 76.105 L 88 72.211 99.750 71.815 C 113.117 71.364, 117.595 69.741, 122.762 63.473 C 128.159 56.925, 129.673 45.269, 126.134 37.500 C 123.787 32.346, 117.218 26.445, 112.132 24.921 C 108.617 23.868, 107.767 22.968, 105.028 17.405 C 99.364 5.901, 89.280 -0.062, 75.712 0.070 C 71.746 0.109, 66.773 0.774, 64.662 1.549 M 67.885 9.380 C 60.093 12.164, 55.057 17.704, 52.527 26.276 C 51.174 30.856, 50.220 31.617, 44.729 32.496 C 37.017 33.729, 30.917 42.446, 32.374 50.154 C 34.239 60.026, 40.582 63.944, 54.750 63.978 L 64 64 64 57.122 C 64 52.457, 64.449 49.872, 65.396 49.086 C 66.310 48.328, 70.370 48.027, 77.146 48.214 L 87.500 48.500 87.794 56.359 L 88.088 64.218 98.989 63.845 C 108.043 63.535, 110.356 63.125, 112.634 61.424 C 119.736 56.122, 121.911 47.667, 118.097 40.190 C 115.870 35.824, 110.154 32.014, 105.790 31.985 C 102.250 31.961, 101.126 30.787, 99.532 25.443 C 95.580 12.197, 80.880 4.736, 67.885 9.380 M 72 70.800 C 72 80.978, 71.625 85.975, 70.800 86.800 C 70.140 87.460, 67.781 88, 65.559 88 L 61.517 88 68.759 95.241 L 76 102.483 83.241 95.241 L 90.483 88 86.441 88 C 84.219 88, 81.860 87.460, 81.200 86.800 C 80.375 85.975, 80 80.978, 80 70.800 L 80 56 76 56 L 72 56 72 70.800 M 25.200 65.200 C 23.566 66.834, 23.566 85.166, 25.200 86.800 C 27.002 88.602, 29.798 88.246, 30.965 86.066 C 31.534 85.002, 32 80.472, 32 76 C 32 71.528, 31.534 66.998, 30.965 65.934 C 29.798 63.754, 27.002 63.398, 25.200 65.200"
                    stroke="none" fill="currentColor" fill-rule="evenodd" />
            </symbol>
            <symbol viewBox="0 0 24 24" id="chatIcon">
                <path fill="currentColor"
                    d="m18 21l-1.4-1.4l1.575-1.6H14v-2h4.175L16.6 14.4L18 13l4 4l-4 4ZM3 21V6q0-.825.588-1.413T5 4h12q.825 0 1.413.588T19 6v5.075q-.25-.05-.5-.063T18 11q-.25 0-.5.013t-.5.062V6H5v10h7.075q-.05.25-.063.5T12 17q0 .25.013.5t.062.5H6l-3 3Zm4-11h8V8H7v2Zm0 4h5v-2H7v2Zm-2 2V6v10Z" />
            </symbol>
            <symbol viewBox="0 0 24 24" id="chatEditIcon">
                <path fill="currentColor"
                    d="M5 19h1.4l8.625-8.625l-1.4-1.4L5 17.6V19ZM19.3 8.925l-4.25-4.2l1.4-1.4q.575-.575 1.413-.575t1.412.575l1.4 1.4q.575.575.6 1.388t-.55 1.387L19.3 8.925ZM17.85 10.4L7.25 21H3v-4.25l10.6-10.6l4.25 4.25Zm-3.525-.725l-.7-.7l1.4 1.4l-.7-.7Z">
                </path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="deleteIcon">
                <path fill="currentColor"
                    d="M8 20v-5h2v5h9v-7H5v7h3zm-4-9h16V8h-6V4h-4v4H4v3zM3 21v-8H2V7a1 1 0 0 1 1-1h5V3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v3h5a1 1 0 0 1 1 1v6h-1v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z">
                </path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="addIcon" stroke="currentColor" fill="none" stroke-width="2"
                stroke-linecap="round" stroke-linejoin="round">
                <line x1="12" y1="5" x2="12" y2="19"></line>
                <line x1="5" y1="12" x2="19" y2="12"></line>
            </symbol>
            <symbol viewBox="0 0 200 100" preserveAspectRatio="xMidYMid" id="loadingIcon">
                <g transform="translate(50 50)">
                    <circle cx="0" cy="0" r="15" fill="#e15b64">
                        <animateTransform attributeName="transform" type="scale" begin="-0.3333333333333333s"
                            calcMode="spline" keySplines="0.3 0 0.7 1;0.3 0 0.7 1" values="0;1;0" keyTimes="0;0.5;1"
                            dur="1s" repeatCount="indefinite"></animateTransform>
                    </circle>
                </g>
                <g transform="translate(100 50)">
                    <circle cx="0" cy="0" r="15" fill="#f8b26a">
                        <animateTransform attributeName="transform" type="scale" begin="-0.16666666666666666s"
                            calcMode="spline" keySplines="0.3 0 0.7 1;0.3 0 0.7 1" values="0;1;0" keyTimes="0;0.5;1"
                            dur="1s" repeatCount="indefinite"></animateTransform>
                    </circle>
                </g>
                <g transform="translate(150 50)">
                    <circle cx="0" cy="0" r="15" fill="#99c959">
                        <animateTransform attributeName="transform" type="scale" begin="0s" calcMode="spline"
                            keySplines="0.3 0 0.7 1;0.3 0 0.7 1" values="0;1;0" keyTimes="0;0.5;1" dur="1s"
                            repeatCount="indefinite"></animateTransform>
                    </circle>
                </g>
            </symbol>
            <symbol viewBox="0 0 24 24" id="exportIcon">
                <path fill="currentColor"
                    d="M4 19h16v-7h2v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-8h2v7zM14 9h5l-7 7l-7-7h5V3h4v6z"></path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="importIcon">
                <path fill="currentColor"
                    d="M4 19h16v-7h2v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-8h2v7zM14 9v6h-4V9H5l7-7l7 7h-5z"></path>
            </symbol>
            <symbol viewBox="0 0 24 24" id="clearAllIcon">
                <path fill="currentColor"
                    d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10zm0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16zm0-9.414l2.828-2.829l1.415 1.415L13.414 12l2.829 2.828l-1.415 1.415L12 13.414l-2.828 2.829l-1.415-1.415L10.586 12L7.757 9.172l1.415-1.415L12 10.586z">
                </path>
            </symbol>
        </svg>
    </div>
    <div id="loadMask">
        <div>
            <div>万事通</div>
            <svg>
                <use xlink:href="#loadingIcon" />
            </svg>
        </div>
    </div>
    <div class="chat_window">
        <div class="overlay"></div>
        <nav class="nav">
            <div id="newChat">
                <svg width="24" height="24">
                    <use xlink:href="#addIcon" />
                </svg>
                <span>新建会话</span>
            </div>
            <div id="chatList"></div>
            <div class="navFooter">
                <div class="navFunc">
                    <div id="exportChat">
                        <svg width="24" height="24">
                            <use xlink:href="#exportIcon" />
                        </svg>
                        <span>导出</span>
                    </div>
                    <label id="importChat" for="importChatInput">
                        <svg width="24" height="24">
                            <use xlink:href="#importIcon" />
                        </svg>
                        <span>导入</span>
                    </label>
                    <input type="file" style="display: none;" id="importChatInput" accept="application/json" />
                    <div id="clearChat">
                        <svg width="24" height="24">
                            <use xlink:href="#clearAllIcon" />
                        </svg>
                        <span>清空</span>
                    </div>
                </div>
                <div class="divider"></div>
                <!-- <div class="links">
                    <a href="https://github.com/xqdoo00o/ChatGPT-web" target="_blank"
                        rel="noopener noreferrer">Github</a>
                </div> -->
            </div>
        </nav>
        <div class="top_menu">
            <div class="toggler">
                <div class="button close"></div>
                <div class="button minimize"></div>
                <div class="button maximize"></div>
            </div>
            <div class="title"></div>
            <button id="setting">
                <svg viewBox="0 0 100 100" style="display:block;" role="img" width="30" height="30">
                    <title>设置</title>
                    <circle cx="50" cy="20" r="10" fill="#e15b64" />
                    <circle cx="50" cy="50" r="10" fill="#f8b26a" />
                    <circle cx="50" cy="80" r="10" fill="#99c959" />
                </svg>
            </button>
            <div id="setDialog" style="display:none;">
                <div class="setSwitch">
                    <div data-id="convOption" class="activeSwitch">对话</div>
                    <div data-id="speechOption">语音合成</div>
                    <div data-id="recOption">语音识别</div>
                </div>
                <div id="convOption">
                    <div class="presetSelect presetModelCls">
                        <label for="preSetModel">GPT模型</label>
                        <select id="preSetModel">
                            <option value="gpt-3.5-turbo">gpt-3.5</option>
                            <option value="gpt-4">gpt-4</option>
                            <option value="gpt-4-32k">gpt-4-32k</option>
                        </select>
                    </div>
                    <div>
                        <div>自定义接口</div>
                        <input class="inputTextClass" placeholder="https://example.com/" id="apiHostInput"
                            value="https://kill136-gpt-proxy.deno.dev/" />
                    </div>
                    <div>
                        <div>自定义API key</div>
                        <input class="inputTextClass" type="password" placeholder="sk-xxxxxx" id="keyInput"
                            style="-webkit-text-security: disc;" />
                    </div>
                    <div>
                        <div class="justSetLine presetSelect">
                            <div>系统角色</div>
                            <div>
                                <label for="preSetSystem">预设角色</label>
                                <select id="preSetSystem">
                                    <option value="">默认</option>
                                    <option value="normal">助手</option>
                                    <option value="cat">猫娘</option>
                                    <option value="emoji">表情</option>
                                    <option value="image">有图</option>
                                </select>
                            </div>
                        </div>
                        <textarea class="inputTextClass areaTextClass" placeholder="你是一个乐于助人的助手，尽量简明扼要地回答"
                            id="systemInput"></textarea>
                    </div>
                    <div>
                        <span>角色性格</span>
                        <input type="range" id="top_p" min="0" max="1" value="1" step="0.05" />
                        <div class="selectDef">
                            <span>准确严谨</span>
                            <span>灵活创新</span>
                        </div>
                    </div>
                    <div>
                        <span>回答质量</span>
                        <input type="range" id="temp" min="0" max="2" value="1" step="0.05" />
                        <div class="selectDef">
                            <span>重复刻板</span>
                            <span>胡言乱语</span>
                        </div>
                    </div>
                    <div>
                        <span>打字机速度</span>
                        <input type="range" id="textSpeed" min="0" max="100" value="88" step="1" />
                        <div class="selectDef">
                            <span>慢</span>
                            <span>快</span>
                        </div>
                    </div>
                    <div>
                        <span class="inlineTitle">连续对话</span>
                        <label class="switch-slide">
                            <input type="checkbox" id="enableCont" checked hidden />
                            <label for="enableCont" class="switch-slide-label"></label>
                        </label>
                    </div>
                    <div>
                        <span class="inlineTitle">长回复</span>
                        <label class="switch-slide">
                            <input type="checkbox" id="enableLongReply" hidden />
                            <label for="enableLongReply" class="switch-slide-label"></label>
                        </label>
                    </div>
                </div>
                <div id="speechOption" style="display: none;">
                    <div class="presetSelect presetModelCls">
                        <label for="preSetService">语音合成服务</label>
                        <select id="preSetService">
                            <option value="3">Azure语音</option>
                            <option selected value="2">Edge语音</option>
                            <option value="1">系统语音</option>
                        </select>
                    </div>
                    <div class="presetSelect presetModelCls">
                        <label for="preSetAzureRegion">Azure 区域</label>
                        <select id="preSetAzureRegion">
                        </select>
                    </div>
                    <div>
                        <div>Azure Access Key</div>
                        <input class="inputTextClass" type="password" placeholder="Azure Key" id="azureKeyInput"
                            style="-webkit-text-security: disc;" />
                    </div>
                    <div id="checkVoiceLoad" style="display: none;">
                        <svg>
                            <use xlink:href="#loadingIcon" />
                        </svg>
                        <span>加载语音</span>
                    </div>
                    <div id="speechDetail">
                        <div>
                            <div class="justSetLine">
                                <div>语音类型</div>
                                <div id="voiceTypes">
                                    <span data-type="0">提问语音</span>
                                    <span data-type="1" class="selVoiceType">回答语音</span>
                                </div>
                            </div>
                            <select id="preSetSpeech">
                            </select>
                        </div>
                        <div class="justSetLine presetSelect" id="azureExtra" style="display:none;">
                            <div class="presetModelCls">
                                <label for="preSetVoiceStyle">风格</label>
                                <select id="preSetVoiceStyle">
                                </select>
                            </div>
                            <div class="presetModelCls">
                                <label for="preSetVoiceRole">角色</label>
                                <select id="preSetVoiceRole">
                                </select>
                            </div>
                        </div>
                        <div>
                            <span>音量</span>
                            <input type="range" id="voiceVolume" min="0" max="1" value="1" step="0.1" />
                            <div class="selectDef">
                                <span>低</span>
                                <span>高</span>
                            </div>
                        </div>
                        <div>
                            <span>语速</span>
                            <input type="range" id="voiceRate" min="0.1" max="2" value="1" step="0.1" />
                            <div class="selectDef">
                                <span>慢</span>
                                <span>快</span>
                            </div>
                        </div>
                        <div>
                            <span>音调</span>
                            <input type="range" id="voicePitch" min="0" max="2" value="1" step="0.1" />
                            <div class="selectDef">
                                <span>平淡</span>
                                <span>起伏</span>
                            </div>
                        </div>
                        <div>
                            <span class="inlineTitle">连续朗读</span>
                            <label class="switch-slide">
                                <input type="checkbox" id="enableContVoice" checked hidden />
                                <label for="enableContVoice" class="switch-slide-label"></label>
                            </label>
                        </div>
                        <div>
                            <span class="inlineTitle">自动朗读</span>
                            <label class="switch-slide">
                                <input type="checkbox" id="enableAutoVoice" checked hidden />
                                <label for="enableAutoVoice" class="switch-slide-label"></label>
                            </label>
                        </div>
                    </div>
                </div>
                <div id="recOption" style="display: none;">
                    <div id="noRecTip" style="display: block;">当前环境不支持语音识别，请尝试使用chrome内核浏览器或重新部署https页面。</div>
                    <div id="yesRec" style="display: none;">
                        <div class="presetSelect presetModelCls">
                            <label for="selectLangOption">语言</label>
                            <select id="selectLangOption">
                            </select>
                        </div>
                        <div class="presetSelect presetModelCls">
                            <label for="selectDiaOption">方言</label>
                            <select id="selectDiaOption">
                            </select>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="messages">
            <div id="chatlog"></div>
            <div id="stopChat"><svg width="24" height="24">
                    <use xlink:href="#stopResIcon" />
                </svg>停止</div>
        </div>
        <div class="bottom_wrapper clearfix">
            <div class="message_input_wrapper">
                <textarea class="message_input_text" spellcheck="false" placeholder="来问点什么吧" id="chatinput"></textarea>
                <div id="voiceRec" style="display:none;">
                    <div id="voiceRecIcon">
                        <svg viewBox="0 0 48 48" id="voiceInputIcon">
                            <g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4">
                                <rect fill="none" width="14" height="27" x="17" y="4" rx="7" />
                                <rect class="animVoice" x="18" y="4" width="12" height="27" stroke="none"
                                    fill="currentColor"></rect>
                                <path stroke-linecap="round"
                                    d="M9 23c0 8.284 6.716 15 15 15c8.284 0 15-6.716 15-15M24 38v6" />
                            </g>
                        </svg>
                    </div>
                    <div id="voiceRecSetting">
                        <select id="select_language" style="margin-bottom: 4px;"></select>
                        <select id="select_dialect"></select>
                    </div>
                </div>
            </div>
            <button class="loaded" id="sendbutton">
                <span>发送</span>
                <svg style="margin:0 auto;height:40px;width:100%;">
                    <use xlink:href="#loadingIcon" />
                </svg>
            </button>
            <button id="clearConv"><svg role="img" width="20px" height="20px">
                    <title>清空会话</title>
                    <use xlink:href="#deleteIcon" />
                </svg></button>
        </div>
    </div>
    <link rel="stylesheet" href="//cdn.staticfile.org/notyf/3.10.0/notyf.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    <link rel="stylesheet" href="index.css">
    <script src="//cdn.staticfile.org/notyf/3.10.0/notyf.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
        //并配置了显示位置在中心顶部。还配置了两种消息类型的样式：成功类型和错误类型，分别对应不同的背景颜色和持续时间。
        //这些设置可以让使用者在进行消息提示时，更容易地区分不同类型的消息。
        const notyf = new Notyf({
            position: {
                x: 'center',
                y: 'top'
            },
            types: [{
                    type: 'success',
                    background: '#99c959',
                    duration: 2000,
                },
                {
                    type: 'error',
                    background: '#e15b64',
                    duration: 3000,
                }
            ]
        });
        //该代码监听了文档中所有的点击事件，如果触发点击事件的元素的类名是 "toggler"，则给 body 标签添加 "show-nav" 类；
        //如果触发点击事件的元素的类名是 "overlay"，则移除 body 标签的 "show-nav" 类；
        //如果触发点击事件的元素就是 body 标签本身，那么也移除 "show-nav" 类。通过这些事件绑定，
        //可以实现在点击按钮时显示或隐藏导航栏，在点击页面其他部分时隐藏导航栏的效果。
        document.body.addEventListener("click", event => {
            if (event.target.className === "toggler") {
                document.body.classList.toggle("show-nav");
            } else if (event.target.className === "overlay") {
                document.body.classList.remove("show-nav");
            } else if (event.target === document.body) {
                document.body.classList.remove("show-nav");
            }
        });

        const messagsEle = document.getElementsByClassName("messages")[0]; //消息
        const chatlog = document.getElementById("chatlog"); //日志
        const stopEle = document.getElementById("stopChat"); //停止聊天
        const sendBtnEle = document.getElementById("sendbutton"); //发送按钮
        const textarea = document.getElementById("chatinput"); //聊天输入框
        const settingEle = document.getElementById("setting"); //设置
        const dialogEle = document.getElementById("setDialog"); //设置对话框
        const systemEle = document.getElementById("systemInput"); //系统输入框
        const speechServiceEle = document.getElementById("preSetService"); //语音预设值
        const newChatEle = document.getElementById("newChat"); //新对话框
        const chatListEle = document.getElementById("chatList"); //聊天列表
        const voiceRecEle = document.getElementById("voiceRecIcon"); //录音
        const voiceRecSetEle = document.getElementById("voiceRecSetting"); //录音识别设置
        //APPID，APISecret，APIKey在控制台-我的应用-语音听写（流式版）页面获取
        const APPID = '8522e32b'
        const API_SECRET = 'MGE4NDM0MmY4MTY1MzJmYjUxMmFkZWNj'
        const API_KEY = '4b33ea6280134dd3de25be526fb35455'
        let voiceType = 1; // 设置 0: 提问语音，1：回答语音
        let voiceRole = []; // 语音
        let voiceVolume = []; //音量
        let voiceRate = []; // 语速
        let voicePitch = []; // 音调
        let enableContVoice; // 连续朗读
        let enableAutoVoice; // 自动朗读
        let existVoice = 2; // 3:Azure在线语音 2:使用edge在线语音, 1:使用本地语音, 0:不支持语音
        let azureToken; //Azure 订阅的密钥，用于身份验证。
        let azureTokenTimer; //Azure 认证信息的有效时间。
        let azureRegion; //Azure 地理位置
        let azureKey; //Azure API key。
        let azureRole = []; //Azure 访问权限。
        let azureStyle = []; //Azure 地图样式配置。
        // https或本地安全环境
        let isSafeEnv = location.hostname.match(/127.|localhost/) || location.protocol.match(/https:|file:/);
        // 是否支持语音识别输入
        //let supportRec = !!window.webkitSpeechRecognition && isSafeEnv;
        let supportRec = false
        //是否正在录制
        let recing = false;
        var workerScript = "(function(){self.onmessage=function(e){t.transcode(e.data)};let t={transcode(e){let r=t.to16kHz(e);r=t.to16BitPCM(r),r=Array.from(new Uint8Array(r.buffer)),self.postMessage(r)},to16kHz(t){var e=new Float32Array(t),r=Math.round(e.length*(16e3/44100)),a=new Float32Array(r),n=(e.length-1)/(r-1);a[0]=e[0];for(let t=1;t<r-1;t++){var o=t*n,l=Math.floor(o).toFixed(),f=Math.ceil(o).toFixed(),h=o-l;a[t]=e[l]+(e[f]-e[l])*h}return a[r-1]=e[e.length-1],a},to16BitPCM(t){for(var e=2*t.length,r=new ArrayBuffer(e),a=new DataView(r),n=0,o=0;o<t.length;o++,n+=2){var l=Math.max(-1,Math.min(1,t[o]));a.setInt16(n,l<0?32768*l:32767*l,!0)}return a}}})();";
        var workerBlob = new Blob([workerScript], { type: "text/javascript" });
        var workerUrl = URL.createObjectURL(workerBlob);
        var transWorker = new Worker(workerUrl);

        //let transWorker = new TransWorker()
        // var transWorker = new Worker('transcode.worker.js');
        let toggleRecEv;
        textarea.focus();
        //当用户在文本框中输入时会调用该函数。在该函数中，首先会检查页面是否正在加载中，如果不是则执行接下来的操作。
        //然后会检查文本框内容是否为空格，如果不是则给发送按钮添加一个类名“activeSendBtn”，否则移除该类名。
        //最后调整文本框的高度，以适应文本框中内容的多少。
        const textInputEvent = () => {
            if (!loading) {
                if (textarea.value.trim().length) {
                    sendBtnEle.classList.add("activeSendBtn");
                } else {
                    sendBtnEle.classList.remove("activeSendBtn");
                }
            }
            textarea.style.height = "47px";
            textarea.style.height = textarea.scrollHeight + "px";
        };

        function getWebSocketUrl() {
            return new Promise((resolve, reject) => {
                // 请求地址根据语种不同变化
                var url = 'wss://iat-api.xfyun.cn/v2/iat'
                var host = 'iat-api.xfyun.cn'
                var apiKey = API_KEY
                var apiSecret = API_SECRET
                var date = new Date().toGMTString()
                var algorithm = 'hmac-sha256'
                var headers = 'host date request-line'
                var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`
                var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)
                var signature = CryptoJS.enc.Base64.stringify(signatureSha)
                var authorizationOrigin =
                    `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`
                var authorization = btoa(authorizationOrigin)
                url = `${url}?authorization=${authorization}&date=${date}&host=${host}`
                resolve(url)
            })
        }
        textarea.oninput = textInputEvent;
        //定义讯飞语音识别类
        class IatRecorder {
            constructor({
                language,
                accent,
                appId
            } = {}) {
                let self = this
                this.status = 'null'
                this.language = language || 'zh_cn'
                this.accent = accent || 'mandarin'
                this.appId = appId || APPID
                // 记录音频数据
                this.audioData = []
                // 记录听写结果
                this.resultText = ''
                // wpgs下的听写结果需要中间状态辅助记录
                this.resultTextTemp = ''
                transWorker.onmessage = function (event) {
                    self.audioData.push(...event.data)
                }
            }
            // 修改录音听写状态
            setStatus(status) {
                this.onWillStatusChange && this.status !== status && this.onWillStatusChange(this.status, status)
                this.status = status
            }
            setResultText({
                resultText,
                resultTextTemp
            } = {}) {
                this.onTextChange && this.onTextChange(resultTextTemp || resultText || '')
                resultText !== undefined && (this.resultText = resultText)
                resultTextTemp !== undefined && (this.resultTextTemp = resultTextTemp)
            }
            // 修改听写参数
            setParams({
                language,
                accent
            } = {}) {
                language && (this.language = language)
                accent && (this.accent = accent)
            }
            // 连接websocket
            connectWebSocket() {
                return getWebSocketUrl().then(url => {
                    let iatWS
                    if ('WebSocket' in window) {
                        iatWS = new WebSocket(url)
                    } else if ('MozWebSocket' in window) {
                        iatWS = new MozWebSocket(url)
                    } else {
                        alert('浏览器不支持WebSocket')
                        return
                    }
                    this.webSocket = iatWS
                    this.setStatus('init')
                    iatWS.onopen = e => {
                        this.setStatus('ing')
                        // 重新开始录音
                        setTimeout(() => {
                            this.webSocketSend()
                        }, 500)
                    }
                    iatWS.onmessage = e => {
                        this.result(e.data)
                    }
                    iatWS.onerror = e => {
                        this.recorderStop()
                    }
                    iatWS.onclose = e => {
                        this.recorderStop()
                    }
                })
            }
            // 初始化浏览器录音
            recorderInit() {
                navigator.getUserMedia =
                    navigator.getUserMedia ||
                    navigator.webkitGetUserMedia ||
                    navigator.mozGetUserMedia ||
                    navigator.msGetUserMedia

                // 创建音频环境
                try {
                    this.audioContext = new(window.AudioContext || window.webkitAudioContext)()
                    this.audioContext.resume()
                    if (!this.audioContext) {
                        alert('浏览器不支持webAudioApi相关接口')
                        return
                    }
                } catch (e) {
                    if (!this.audioContext) {
                        alert('浏览器不支持webAudioApi相关接口')
                        return
                    }
                }

                // 获取浏览器录音权限
                if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                    navigator.mediaDevices
                        .getUserMedia({
                            audio: true,
                            video: false,
                        })
                        .then(stream => {
                            getMediaSuccess(stream)
                        })
                        .catch(e => {
                            getMediaFail(e)
                        })
                } else if (navigator.getUserMedia) {
                    navigator.getUserMedia({
                            audio: true,
                            video: false,
                        },
                        stream => {
                            getMediaSuccess(stream)
                        },
                        function (e) {
                            getMediaFail(e)
                        }
                    )
                } else {
                    if (navigator.userAgent.toLowerCase().match(/chrome/) && location.origin.indexOf('https://') <
                        0) {
                        alert('chrome下获取浏览器录音功能，因为安全性问题，需要在localhost或127.0.0.1或https下才能获取权限')
                    } else {
                        alert('无法获取浏览器录音功能，请升级浏览器或使用chrome')
                    }
                    this.audioContext && this.audioContext.close()
                    return
                }
                // 获取浏览器录音权限成功的回调
                let getMediaSuccess = stream => {
                    console.log('getMediaSuccess')
                    // 创建一个用于通过JavaScript直接处理音频
                    this.scriptProcessor = this.audioContext.createScriptProcessor(0, 1, 1)
                    this.scriptProcessor.onaudioprocess = e => {
                        // 去处理音频数据
                        if (this.status === 'ing') {
                            transWorker.postMessage(e.inputBuffer.getChannelData(0))
                        }
                    }
                    // 创建一个新的MediaStreamAudioSourceNode 对象，使来自MediaStream的音频可以被播放和操作
                    this.mediaSource = this.audioContext.createMediaStreamSource(stream)
                    // 连接
                    this.mediaSource.connect(this.scriptProcessor)
                    this.scriptProcessor.connect(this.audioContext.destination)
                    this.connectWebSocket()
                }

                let getMediaFail = (e) => {
                    alert('请求麦克风失败')
                    console.log(e)
                    this.audioContext && this.audioContext.close()
                    this.audioContext = undefined
                    // 关闭websocket
                    if (this.webSocket && this.webSocket.readyState === 1) {
                        this.webSocket.close()
                    }
                }
            }
            recorderStart() {
                if (!this.audioContext) {
                    this.recorderInit()
                } else {
                    this.audioContext.resume()
                    this.connectWebSocket()
                }
            }
            // 暂停录音
            recorderStop() {
                // safari下suspend后再次resume录音内容将是空白，设置safari下不做suspend
                if (!(/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgen))) {
                    this.audioContext && this.audioContext.suspend()
                }
                this.setStatus('end')
            }
            // 处理音频数据
            // transAudioData(audioData) {
            //   audioData = transAudioData.transaction(audioData)
            //   this.audioData.push(...audioData)
            // }
            // 对处理后的音频数据进行base64编码，
            toBase64(buffer) {
                var binary = ''
                var bytes = new Uint8Array(buffer)
                var len = bytes.byteLength
                for (var i = 0; i < len; i++) {
                    binary += String.fromCharCode(bytes[i])
                }
                return window.btoa(binary)
            }
            // 向webSocket发送数据
            webSocketSend() {
                if (this.webSocket.readyState !== 1) {
                    return
                }
                let audioData = this.audioData.splice(0, 1280)
                var params = {
                    common: {
                        app_id: this.appId,
                    },
                    business: {
                        language: this.language, //小语种可在控制台--语音听写（流式）--方言/语种处添加试用
                        domain: 'iat',
                        accent: this.accent, //中文方言可在控制台--语音听写（流式）--方言/语种处添加试用
                        vad_eos: 5000,
                        dwa: 'wpgs', //为使该功能生效，需到控制台开通动态修正功能（该功能免费）
                    },
                    data: {
                        status: 0,
                        format: 'audio/L16;rate=16000',
                        encoding: 'raw',
                        audio: this.toBase64(audioData),
                    },
                }
                this.webSocket.send(JSON.stringify(params))
                this.handlerInterval = setInterval(() => {
                    // websocket未连接
                    if (this.webSocket.readyState !== 1) {
                        this.audioData = []
                        clearInterval(this.handlerInterval)
                        return
                    }
                    if (this.audioData.length === 0) {
                        if (this.status === 'end') {
                            this.webSocket.send(
                                JSON.stringify({
                                    data: {
                                        status: 2,
                                        format: 'audio/L16;rate=16000',
                                        encoding: 'raw',
                                        audio: '',
                                    },
                                })
                            )
                            this.audioData = []
                            clearInterval(this.handlerInterval)
                        }
                        return false
                    }
                    audioData = this.audioData.splice(0, 1280)
                    // 中间帧
                    this.webSocket.send(
                        JSON.stringify({
                            data: {
                                status: 1,
                                format: 'audio/L16;rate=16000',
                                encoding: 'raw',
                                audio: this.toBase64(audioData),
                            },
                        })
                    )
                }, 40)
            }
            result(resultData) {
                // 识别结束
                let jsonData = JSON.parse(resultData)
                if (jsonData.data && jsonData.data.result) {
                    let data = jsonData.data.result
                    let str = ''
                    let resultStr = ''
                    let ws = data.ws
                    for (let i = 0; i < ws.length; i++) {
                        str = str + ws[i].cw[0].w
                    }
                    // 开启wpgs会有此字段(前提：在控制台开通动态修正功能)
                    // 取值为 "apd"时表示该片结果是追加到前面的最终结果；取值为"rpl" 时表示替换前面的部分结果，替换范围为rg字段
                    if (data.pgs) {
                        if (data.pgs === 'apd') {
                            // 将resultTextTemp同步给resultText
                            this.setResultText({
                                resultText: this.resultTextTemp,
                            })
                        }
                        // 将结果存储在resultTextTemp中
                        this.setResultText({
                            resultTextTemp: this.resultText + str,
                        })
                    } else {
                        this.setResultText({
                            resultText: this.resultText + str,
                        })
                    }
                }
                if (jsonData.code === 0 && jsonData.data.status === 2) {
                    this.webSocket.close()
                }
                if (jsonData.code !== 0) {
                    this.webSocket.close()
                    console.log(`${jsonData.code}:${jsonData.message}`)
                }
            }
            start() {
                this.recorderStart()
                this.setResultText({
                    resultText: '',
                    resultTextTemp: ''
                })
            }
            stop() {
                this.recorderStop()
            }
        }
        //是否支持录制语音
        //如果支持语音识别，会执行一系列操作。首先隐藏不需要显示的提示信息，显示需要显示的元素，然后给文本框添加一个类名"message_if_voice"。
        //接着，根据一定的语言列表，创建下拉选择框，并设置默认值，当选择不同的语言时，也会相应更改下拉选择框的内容。
        //根据用户的语言设置，可以选择不同的语音输入方式。当语音输入时，会创建一个webkitSpeechRecognition实例，然后进行相关设置和事件绑定。
        //当出现错误时，会给用户相应的提示信息。当语音输入完成后，将文本框中的值更改为识别到的值。用户还可以选择进行长按语音输入，
        //打开语音输入设置界面。最后，还设置了鼠标和触摸事件来触发语音输入。
        if (supportRec) {
            noRecTip.style.display = "none";
            yesRec.style.display = "block";
            document.getElementById("voiceRec").style.display = "block";
            textarea.classList.add("message_if_voice");
            let langs = [ // from https://www.google.com/intl/en/chrome/demos/speech.html
                ['中文', ['cmn-Hans-CN', '普通话 (大陆)'],
                    ['cmn-Hans-HK', '普通话 (香港)'],
                    ['cmn-Hant-TW', '中文 (台灣)'],
                    ['yue-Hant-HK', '粵語 (香港)']
                ],
                ['English', ['en-US', 'United States'],
                    ['en-GB', 'United Kingdom'],
                    ['en-AU', 'Australia'],
                    ['en-CA', 'Canada'],
                    ['en-IN', 'India'],
                    ['en-KE', 'Kenya'],
                    ['en-TZ', 'Tanzania'],
                    ['en-GH', 'Ghana'],
                    ['en-NZ', 'New Zealand'],
                    ['en-NG', 'Nigeria'],
                    ['en-ZA', 'South Africa'],
                    ['en-PH', 'Philippines']
                ]
            ];
            //根据一定的语言列表，创建下拉选择框，并设置默认值，当选择不同的语言时，也会相应更改下拉选择框的内容
            langs.forEach((lang, i) => {
                select_language.options.add(new Option(lang[0], i));
                selectLangOption.options.add(new Option(lang[0], i))
            });
            //当用户选择不同的语言选项时，会执行该函数。在该函数中，首先更新选择下拉列表中的选中项，然后将方言下拉列表中的内容清空，
            //根据所选择的语言，添加新的方言选项。同时，通过判断方言选项中的内容长度，确定是否显示该下拉列表；如果方言选项中只有一个选项，
            //则隐藏该下拉列表。最后，将所选择的方言的值存储到LocalStorage中。
            const updateCountry = function () {
                selectLangOption.selectedIndex = select_language.selectedIndex = this.selectedIndex;
                select_dialect.innerHTML = "";
                selectDiaOption.innerHTML = "";
                let list = langs[select_language.selectedIndex];
                for (let i = 1; i < list.length; i++) {
                    select_dialect.options.add(new Option(list[i][1], list[i][0]));
                    selectDiaOption.options.add(new Option(list[i][1], list[i][0]));
                }
                select_dialect.style.visibility = list[1].length == 1 ? 'hidden' : 'visible';
                selectDiaOption.parentElement.style.visibility = list[1].length == 1 ? 'hidden' : 'visible';
                localStorage.setItem("voiceRecLang", select_dialect.value);
            };
            let localLangIdx = 0;
            let localDiaIdx = 0;
            //获取语音识别的设置
            let localRecLang = localStorage.getItem("voiceRecLang");
            if (localRecLang) {
                let localIndex = langs.findIndex(item => {
                    let diaIdx = item.findIndex(lang => {
                        return lang instanceof Array && lang[0] === localRecLang
                    });
                    if (diaIdx !== -1) {
                        localDiaIdx = diaIdx - 1;
                        return true;
                    }
                    return false;
                });
                if (localIndex !== -1) {
                    localLangIdx = localIndex;
                }
            }
            selectLangOption.onchange = updateCountry;
            select_language.onchange = updateCountry;
            selectDiaOption.onchange = select_dialect.onchange = function () {
                selectDiaOption.selectedIndex = select_dialect.selectedIndex = this.selectedIndex;
                localStorage.setItem("voiceRecLang", select_dialect.value);
            }
            selectLangOption.selectedIndex = select_language.selectedIndex = localLangIdx;
            select_language.dispatchEvent(new Event("change"));
            selectDiaOption.selectedIndex = select_dialect.selectedIndex = localDiaIdx;
            select_dialect.dispatchEvent(new Event("change"));
            //webkitSpeechRecognition是谷歌浏览器提供的语音识别API，可以在前端实现语音输入功能。创建该对象后，
            //可以对其进行参数设置，例如设置语言、识别模式、识别结果的最大数量等。对recIns对象进行设置后，
            //可以调用start()方法开始语音输入。在录音期间，调用onresult()方法来获取识别结果。
            let recIns = new webkitSpeechRecognition();
            // prevent some Android bug
            recIns.continuous = !(/\bAndroid\b/i.test(navigator.userAgent));
            recIns.interimResults = true;
            recIns.maxAlternatives = 1;
            let recRes = tempRes = "";
            let oriRes; //记录文本框的初始值
            //如果支持语音输入进入页面 自动启动语音
            //用于在语音输入结束时清除一些变量和状态
            const endEvent = (event) => {
                if (!(event && event.type === "end")) {
                    recIns.stop();
                }
                recIns.onresult = null;
                recIns.onerror = null;
                recIns.onend = null;
                recRes = tempRes = "";
                voiceRecEle.classList.remove("voiceRecing");
                recing = false;
                //自动发送 识别的结果给chatgpt
                genFunc();
            };
            //获取语音识别结果。
            const resEvent = (event) => {
                if (typeof (event.results) === 'undefined') {
                    toggleRecEvent();
                    return;
                }
                let isFinal;
                for (let i = event.resultIndex; i < event.results.length; ++i) {
                    isFinal = event.results[i].isFinal;
                    if (isFinal) {
                        //本次识别到全部文字
                        recRes += event.results[i][0].transcript;
                        console.log(recRes)
                        textarea.value = oriRes + (isFinal ? recRes : tempRes);
                        textInputEvent();
                        textarea.focus();
                        endEvent();
                    } else {
                        tempRes = recRes + event.results[i][0].transcript;
                        console.log(tempRes)
                    }
                }

            };
            //识别错误的时候 处理
            const errorEvent = (ev) => {
                //debugger
                if (event.error === 'no-speech') {
                    notyf.error("未识别到语音，请调整设备后重试！")
                }
                if (event.error === 'audio-capture') {
                    notyf.error("未找到麦克风，请确保已安装麦克风！")
                }
                if (event.error === 'network') {
                    supportRec = false
                    notyf.error("无法国外访问网络，识别失败！")
                }
                if (event.error === 'not-allowed') {
                    notyf.error("未允许使用麦克风的权限！")
                }
                endEvent();
            }
            //关闭语音输入设置界面的事件处理程序
            const closeEvent = (ev) => {
                if (voiceRecSetEle.contains(ev.target)) return;
                if (!voiceRecSetEle.contains(ev.target)) {
                    voiceRecSetEle.style.display = "none";
                    document.removeEventListener("mousedown", closeEvent, true);
                    voiceRecEle.classList.remove("voiceLong");
                }
            }
            //显示语音设置界面 并绑定 鼠标按键取消按钮
            const longEvent = () => {
                voiceRecSetEle.style.display = "block";
                document.addEventListener("mousedown", closeEvent, true);
            }
            //开启录音和结束录音来回切换
            const toggleRecEvent = () => {
                //debugger
                voiceRecEle.classList.toggle("voiceRecing");
                if (voiceRecEle.classList.contains("voiceRecing")) {
                    try {
                        oriRes = textarea.value;
                        recIns.lang = select_dialect.value;
                        recIns.start();
                        recIns.onresult = resEvent;
                        recIns.onerror = errorEvent;
                        recIns.onend = endEvent;
                        recing = true;
                    } catch (e) {
                        endEvent();
                    }
                } else {
                    endEvent();
                }
            };
            window.toggleRecEvent = toggleRecEvent;
            toggleRecEv = toggleRecEvent;
            //首次进入触发录音
            toggleRecEvent();
            let timer;
            //长时间按下录音键 自动调用出来语音设置界面
            const voiceDownEvent = (ev) => {
                ev.preventDefault();
                let i = 0;
                voiceRecEle.classList.add("voiceLong");
                timer = setInterval(() => {
                    i += 1;
                    if (i >= 3) {
                        clearInterval(timer);
                        timer = void 0;
                        longEvent();
                    }
                }, 100)
            };
            //通常是鼠标抬起事件 开启录音 隐藏面板
            const voiceUpEvent = (ev) => {
                ev.preventDefault();
                if (timer !== void 0) {
                    toggleRecEvent();
                    clearInterval(timer);
                    timer = void 0;
                    voiceRecEle.classList.remove("voiceLong");
                }
            }
            voiceRecEle.addEventListener("mousedown", voiceDownEvent);
            voiceRecEle.addEventListener("touchstart", voiceDownEvent);
            voiceRecEle.addEventListener("mouseup", voiceUpEvent);
            voiceRecEle.addEventListener("touchend", voiceUpEvent);
        } else { //如果不支持需要调用讯飞的语音识别
            noRecTip.style.display = "none";
            yesRec.style.display = "block";
            document.getElementById("voiceRec").style.display = "block";
            textarea.classList.add("message_if_voice");
            let localLangIdx = 0;
            let localDiaIdx = 0;
            let countInterval
            let iatRecorder = new IatRecorder()
            // 状态改变时触发
            iatRecorder.onWillStatusChange = function (oldStatus, status) {
                // 可以在这里进行页面中一些交互逻辑处理：倒计时（听写只有60s）,录音的动画，按钮交互等
                let senconds = 0
                if (status === 'ing') {
                    console.log('正在录制' + status)
                    voiceRecEle.classList.toggle("voiceRecing");
                    // 启动倒计时相关
                    // countInterval = setInterval(() => {
                    //     senconds++
                    //     $('.used-time').text(
                    //         `0${Math.floor(senconds/60)}：${Math.floor(senconds/10)}${senconds%10}`)
                    //     if (senconds >= 60) {
                    //         this.stop()
                    //         $('.used-time').text('00：00')
                    //         clearInterval(countInterval)
                    //     }
                    // }, 1000)
                } else if (status === 'init') {
                    console.log(status)
                    // $('.used-time').text('00：00')
                } else if (status === 'end') {
                    // console.log('识别结束  清理定时器' + status)
                    // endEvent()
                    // $('.used-time').text('00：00')
                    // clearInterval(countInterval)
                } else {
                    //识别结束  清理定时器
                    console.log('默认' + status)
                    // clearInterval(countInterval)
                }
            }
            // 监听识别结果的变化
            iatRecorder.onTextChange = function (text) {
                textarea.value = text
            }
            //用于在语音输入结束时清除一些变量和状态
            const endEvent = (event) => {
                // if (!(event && event.type === "end")) {
                //     iatRecorder.stop();
                // }
                voiceRecEle.classList.remove("voiceRecing");
                recing = false;
                //自动发送 识别的结果给chatgpt
                genFunc();
            };
            const RecButtonEvent = () => {
                if (iatRecorder.status === 'ing') {
                    iatRecorder.stop()
                } else {
                    try {
                        iatRecorder.start()
                        recing = true;
                    } catch (e) {
                        endEvent();
                    }
                }
            };
            window.toggleRecEvent = RecButtonEvent;
            toggleRecEv = RecButtonEvent;
            //显示语音设置界面 并绑定 鼠标按键取消按钮
            const longEvent = () => {
                voiceRecSetEle.style.display = "block";
                document.addEventListener("mousedown", closeEvent, true);
            }
            //长时间按下录音键 自动调用出来语音设置界面
            const voiceDownEvent = (ev) => {
                ev.preventDefault();
                let i = 0;
                voiceRecEle.classList.add("voiceLong");
                timer = setInterval(() => {
                    i += 1;
                    if (i >= 3) {
                        clearInterval(timer);
                        timer = void 0;
                        longEvent();
                    }
                }, 100)
            };
            //通常是鼠标抬起事件 开启录音 隐藏面板
            const voiceUpEvent = (ev) => {
                ev.preventDefault();
                if (timer !== void 0) {
                    RecButtonEvent();
                    clearInterval(timer);
                    timer = void 0;
                    voiceRecEle.classList.remove("voiceLong");
                }
            }
            voiceRecEle.addEventListener("mousedown", voiceDownEvent);
            voiceRecEle.addEventListener("touchstart", voiceDownEvent);
            voiceRecEle.addEventListener("mouseup", voiceUpEvent);
            voiceRecEle.addEventListener("touchend", voiceUpEvent);
        }
        //如果点击的是 对话 则隐藏其他几个选项卡
        document.getElementsByClassName("setSwitch")[0].onclick = function (ev) {
            let activeEle = this.getElementsByClassName("activeSwitch")[0];
            if (ev.target !== activeEle) {
                activeEle.classList.remove("activeSwitch");
                ev.target.classList.add("activeSwitch");
                document.getElementById(ev.target.dataset.id).style.display = "block";
                document.getElementById(activeEle.dataset.id).style.display = "none";
            }
        }
        //从浏览器的本地存储中获取名为"existVoice"的值，如果存在则将其转成整数并存储在叫做"existVoice"的变量中
        //如果"existVoice"变量存在，将其值赋给一个名为"speechServiceEle"的表单元素中。
        let localVoiceType = localStorage.getItem("existVoice");
        if (localVoiceType) {
            existVoice = parseInt(localVoiceType);
            speechServiceEle.value = existVoice;
        }
        //如果浏览器不支持语音合成，将删除"speechServiceEle"表单元素的第二个选项。
        //这里的"window.speechSynthesis"和"window.SpeechSynthesisUtterance"用于检查当前浏览器是否支持语音合成功能。
        //如果不支持，该代码会删除"speechServiceEle"表单元素的第二个选项，以避免用户使得无效的选择。
        if (!(window.speechSynthesis && window.SpeechSynthesisUtterance)) {
            speechServiceEle.remove(2);
        }
        //这段代码的目的是重置和清除Azure语音服务的配置，并隐藏与之相关的所有元素和视图。
        const clearAzureVoice = () => {
            azureKey = void 0;
            azureRegion = void 0;
            azureRole = [];
            azureStyle = [];
            document.getElementById("azureExtra").style.display = "none";
            azureKeyInput.parentElement.style.display = "none";
            preSetAzureRegion.parentElement.style.display = "none";
            if (azureTokenTimer) {
                clearInterval(azureTokenTimer);
                azureTokenTimer = void 0;
            }
        }
        //语音合成服务 切换事件
        speechServiceEle.onchange = () => {
            existVoice = parseInt(speechServiceEle.value);
            localStorage.setItem("existVoice", existVoice);
            toggleVoiceCheck(true);
            if (checkAzureAbort && !checkAzureAbort.signal.aborted) {
                checkAzureAbort.abort();
                checkAzureAbort = void 0;
            }
            if (checkEdgeAbort && !checkEdgeAbort.signal.aborted) {
                checkEdgeAbort.abort();
                checkEdgeAbort = void 0;
            }
            if (existVoice === 3) {
                azureKeyInput.parentElement.style.display = "block";
                preSetAzureRegion.parentElement.style.display = "block";
                loadAzureVoice();
            } else if (existVoice === 2) {
                //使用edge在线语音
                clearAzureVoice();
                loadEdgeVoice();
            } else if (existVoice === 1) {
                //系统语音
                toggleVoiceCheck(false);
                clearAzureVoice();
                loadLocalVoice();
            }
        }


        let azureVoiceData, edgeVoiceData, systemVoiceData, checkAzureAbort, checkEdgeAbort;
        //azure 需要检验key 通过后加载语音类型
        const toggleVoiceCheck = (bool) => {
            checkVoiceLoad.style.display = bool ? "flex" : "none";
            speechDetail.style.display = bool ? "none" : "block";
        }
        //加载Azure音频
        const loadAzureVoice = () => {
            let checking = false;
            //点击校验按钮
            checkVoiceLoad.onclick = () => {
                if (checking) return;
                if (azureKey) {
                    checking = true;
                    checkVoiceLoad.classList.add("voiceChecking");
                    if (azureTokenTimer) {
                        clearInterval(azureTokenTimer);
                    }
                    checkAzureAbort = new AbortController();
                    setTimeout(() => {
                        if (checkAzureAbort && !checkAzureAbort.signal.aborted) {
                            checkAzureAbort.abort();
                            checkAzureAbort = void 0;
                        }
                    }, 15000);
                    Promise.all([getAzureToken(checkAzureAbort.signal), getVoiceList(checkAzureAbort.signal)])
                        .then(() => {
                            azureTokenTimer = setInterval(() => {
                                getAzureToken();
                            }, 540000);
                            toggleVoiceCheck(false);
                        }).catch(e => {}).finally(() => {
                            checkVoiceLoad.classList.remove("voiceChecking");
                            checking = false;
                        })
                }
            }
            //使用key换区token
            const getAzureToken = (signal) => {
                return new Promise((res, rej) => {
                    fetch("https://" + azureRegion +
                        ".api.cognitive.microsoft.com/sts/v1.0/issueToken", {
                            signal,
                            method: "POST",
                            headers: {
                                'Ocp-Apim-Subscription-Key': azureKey
                            }
                        }).then(response => {
                        response.text().then(text => {
                            try {
                                let json = JSON.parse(text);
                                notyf.error("由于订阅密钥无效或 API 端点错误，访问被拒绝！");
                                rej();
                            } catch (e) {
                                azureToken = text;
                                res();
                            }
                        });
                    }).catch(e => {
                        rej();
                    })
                })
            };
            //获取支持的语音列表
            const getVoiceList = (signal) => {
                return new Promise((res, rej) => {
                    if (azureVoiceData) {
                        initVoiceSetting(azureVoiceData);
                        res();
                    } else {
                        let localAzureVoiceData = localStorage.getItem(azureRegion + "voiceData");
                        if (localAzureVoiceData) {
                            azureVoiceData = JSON.parse(localAzureVoiceData);
                            initVoiceSetting(azureVoiceData);
                            res();
                        } else {
                            fetch("https://" + azureRegion +
                                ".tts.speech.microsoft.com/cognitiveservices/voices/list", {
                                    signal,
                                    headers: {
                                        'Ocp-Apim-Subscription-Key': azureKey
                                    }
                                }).then(response => {
                                response.json().then(json => {
                                    azureVoiceData = json;
                                    localStorage.setItem(azureRegion + "voiceData", JSON
                                        .stringify(json));
                                    initVoiceSetting(json);
                                    res();
                                }).catch(e => {
                                    notyf.error("由于订阅密钥无效或 API 端点错误，访问被拒绝！");
                                    rej();
                                })
                            }).catch(e => {
                                rej();
                            })
                        }
                    }
                })
            };
            //初始化语音区域
            let azureRegionEle = document.getElementById("preSetAzureRegion");
            if (!azureRegionEle.options.length) {
                const azureRegions = ['southafricanorth', 'eastasia', 'southeastasia', 'australiaeast',
                    'centralindia', 'japaneast', 'japanwest', 'koreacentral', 'canadacentral', 'northeurope',
                    'westeurope', 'francecentral', 'germanywestcentral', 'norwayeast', 'switzerlandnorth',
                    'switzerlandwest', 'uksouth', 'uaenorth', 'brazilsouth', 'centralus', 'eastus', 'eastus2',
                    'northcentralus', 'southcentralus', 'westcentralus', 'westus', 'westus2', 'westus3'
                ];
                azureRegions.forEach((region, i) => {
                    let option = document.createElement("option");
                    option.value = region;
                    option.text = region;
                    azureRegionEle.options.add(option);
                });
            }
            //加载历史区域
            let localAzureRegion = localStorage.getItem("azureRegion");
            if (localAzureRegion) {
                azureRegion = localAzureRegion;
                azureRegionEle.value = localAzureRegion;
            }
            //区域变化的事件
            azureRegionEle.onchange = () => {
                azureRegion = azureRegionEle.value;
                localStorage.setItem("azureRegion", azureRegion);
                toggleVoiceCheck(true);
            }
            azureRegionEle.dispatchEvent(new Event("change"));
            let azureKeyEle = document.getElementById("azureKeyInput");
            let localAzureKey = localStorage.getItem("azureKey");
            if (localAzureKey) {
                azureKey = localAzureKey;
                azureKeyEle.value = localAzureKey;
            }
            //当修改了key 重新加载设置
            azureKeyEle.onchange = () => {
                azureKey = azureKeyEle.value;
                localStorage.setItem("azureKey", azureKey);
                toggleVoiceCheck(true);
            }
            //在“azureKeyEle”元素上触发一个“change”事件，并检查“azureKey”变量的值。如果“azureKey”存在，
            //则在“checkVoiceLoad”元素上触发一个“click”事件。这段代码通常用于在输入Azure密钥后自动检查语音服务是否可以正常加载，
            //以便确保用户输入的密钥是正确的，并且语音服务可以正常连接。可以在页面加载完成后或用户输入Azure密钥后执行此代码。
            azureKeyEle.dispatchEvent(new Event("change"));
            if (azureKey) {
                checkVoiceLoad.dispatchEvent(new Event("click"))
            }
        }
        //加载Edge语音
        const loadEdgeVoice = () => {
            let checking = false;
            //结束校验
            const endCheck = () => {
                checkVoiceLoad.classList.remove("voiceChecking");
                checking = false;
            };
            //点击开始校验
            //它会在用户点击“checkVoiceLoad”元素时执行。首先它会检查变量“checking”的值，如果为true，那么函数将立即返回，不做任何操作。
            //接下来，函数会将“checking”变量的值设置为true，并将“checkVoiceLoad”元素的CSS类设置为“voiceChecking”，以此告诉用户正在进行检查。
            //然后，代码会检查是否存在“edgeVoiceData”变量的缓存。如果存在，则会初始化语音设置并禁用语音检查。
            //如果不存在，则会发起一个异步请求来获取语音服务的数据。在执行异步请求期间，函数会启动一个超时定时器来检查请求是否超时。
            //如果请求超时了，函数将会中止请求。如果请求成功，则该函数会将获取的语音数据存储到“edgeVoiceData”变量中，并初始化语音设置，
            //然后使用“toggleVoiceCheck(false)”禁用语音检查。在最后，函数会执行“endCheck”函数来完成语音检查。如果请求失败了，
            //则直接执行函数“endCheck”来完成语音检查，同时打印出错误信息。
            checkVoiceLoad.onclick = () => {
                if (checking) return;
                checking = true;
                checkVoiceLoad.classList.add("voiceChecking");
                if (edgeVoiceData) {
                    initVoiceSetting(edgeVoiceData);
                    toggleVoiceCheck(false);
                    endCheck();
                } else {
                    checkEdgeAbort = new AbortController();
                    setTimeout(() => {
                        if (checkEdgeAbort && !checkEdgeAbort.signal.aborted) {
                            checkEdgeAbort.abort();
                            checkEdgeAbort = void 0;
                        }
                    }, 10000);
                    fetch("https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=6A5AA1D4EAFF4E9FB37E23D68491D6F4", {
                        signal: checkEdgeAbort.signal
                    }).then(response => {
                        response.json().then(json => {
                            edgeVoiceData = json;
                            toggleVoiceCheck(false);
                            initVoiceSetting(json);
                            endCheck();
                        });
                    }).catch(err => {
                        endCheck();
                    })
                }
            };
            checkVoiceLoad.dispatchEvent(new Event("click"));
        };
        //加载浏览器本地的音频设置
        //用于加载本地的系统语音列表。函数首先会检查变量“systemVoiceData”是否存在缓存语音列表。
        //如果存在，它会调用“initVoiceSetting”函数进行语音设置的初始化。如果不存在，则该函数将启动一个获取本地语音列表的异步任务来获取语音列表。
        //任务中，函数会定期使用“speechSynthesis.getVoices()”API获取当前系统的语音列表，并检查列表是否为空。
        //如果语音列表不为空，则代表异步任务成功获取了语音列表，此时调用“initVoiceSetting”函数完成语音设置初始化，
        //并将语音列表缓存到“systemVoiceData”变量中。如果异步任务仍未获取语音列表，
        //则下一步检查是否支持“onvoiceschanged”事件或“voiceschanged”事件（语音列表改变事件），
        //如果支持，则绑定该事件，并在事件被触发时重新获取语音列表。如果不支持该事件，则使用定时器定时重新获取语音列表，
        //直到列表不为空或者超过1秒。最后，函数会将获取到的语音列表缓存到“systemVoiceData”中，然后执行“initVoiceSetting”函数进行语音设置初始化
        const loadLocalVoice = () => {
            if (systemVoiceData) {
                initVoiceSetting(systemVoiceData);
            } else {
                let initedVoice = false;
                const getLocalVoice = () => {
                    let voices = speechSynthesis.getVoices();
                    if (voices.length) {
                        if (!initedVoice) {
                            initedVoice = true;
                            systemVoiceData = voices;
                            initVoiceSetting(voices);
                        }
                        return true;
                    } else {
                        return false;
                    }
                }
                let syncExist = getLocalVoice();
                if (!syncExist) {
                    if ("onvoiceschanged" in speechSynthesis) {
                        speechSynthesis.onvoiceschanged = () => {
                            getLocalVoice();
                        }
                    } else if (speechSynthesis.addEventListener) {
                        speechSynthesis.addEventListener("voiceschanged", () => {
                            getLocalVoice();
                        })
                    }
                    let timeout = 0;
                    let timer = setInterval(() => {
                        if (getLocalVoice() || timeout > 1000) {
                            if (timeout > 1000) {
                                existVoice = 0;
                            }
                            clearInterval(timer);
                            timer = null;
                        }
                        timeout += 300;
                    }, 300)
                }
            }
        };
        //初始化音频设置
        const initVoiceSetting = (voices) => {
            //判断是否使用在线语音
            let isOnline = existVoice >= 2;
            let voicesEle = document.getElementById("preSetSpeech");
            // 支持中文和英文 提取中文和英文
            voices = isOnline ? voices.filter(item => item.Locale.match(/^(zh-|en-)/)) : voices.filter(item => item
                .lang.match(/^(zh-|en-)/));
            //在线语音翻译一下名称 统一格式
            if (isOnline) {
                voices.map(item => {
                    item.name = item.FriendlyName || (
                        `${item.DisplayName} Online (${item.VoiceType}) - ${item.LocaleName}`);
                    item.lang = item.Locale;
                })
            }
            //排序把中文的放到前面
            voices.sort((a, b) => {
                if (a.lang.slice(0, 2) === b.lang.slice(0, 2)) return 0;
                return (a.lang < b.lang) ? 1 : -1; // 中文在前"z"
            });
            //隐藏谷歌微软的信息
            voices.map(item => {
                if (item.name.match(/^(Google |Microsoft )/)) {
                    item.displayName = item.name.replace(/^.*? /, "");
                } else {
                    item.displayName = item.name;
                }
            });
            voicesEle.innerHTML = "";
            //填充人声语音列表
            voices.forEach((voice, i) => {
                let option = document.createElement("option");

                option.value = i;
                option.text = voice.displayName;
                if (voice.ShortName == "zh-CN-XiaoyiNeural") {
                    //debugger
                    option.selected = true
                }
                voicesEle.options.add(option);
            });
            //切换语音人声 角色变化
            //在声音角色对象中存储所选声音角色。



            //更新本地存储中的声音角色和声音风格设置。
            //如果声音角色对象不具有 StyleList 或 RolePlayList 属性，则隐藏 Azure 扩展部分，并从本地存储中删除相关设置。
            voicesEle.onchange = () => {
                //debugger
                //add azure tts setting
                // 0: 提问语音，1：回答语音
                voiceRole[voiceType] = voices[voicesEle.value];
                //在本地存储中存储所选声音角色的名称。
                localStorage.setItem("voice" + voiceType, voiceRole[voiceType].name);
                //如果声音角色对象具有 StyleList 或 RolePlayList 属性，则显示 Azure 扩展部分。

                if (voiceRole[voiceType].StyleList || voiceRole[voiceType].RolePlayList) {
                    document.getElementById("azureExtra").style.display = "block";
                    let voiceStyles = voiceRole[voiceType].StyleList;
                    let voiceRoles = voiceRole[voiceType].RolePlayList;
                    //在 Azure 扩展部分中，根据所选声音角色对象的属性，显示预设声音角色和预设声音风格下拉菜单，并根据本地存储中的设置设置其值。
                    if (voiceRoles) {
                        preSetVoiceRole.innerHTML = "";
                        voiceRoles.forEach((role, i) => {
                            let option = document.createElement("option");
                            option.value = role;
                            option.text = role;
                            preSetVoiceRole.options.add(option);
                        });
                        let localRole = localStorage.getItem("azureRole" + voiceType);
                        if (localRole && voiceRoles.indexOf(localRole) !== -1) {
                            preSetVoiceRole.value = localRole;
                            azureRole[voiceType] = localRole;
                        } else {
                            preSetVoiceRole.selectedIndex = 0;
                            azureRole[voiceType] = voiceRole[0];
                        }
                        preSetVoiceRole.onchange = () => {
                            azureRole[voiceType] = preSetVoiceRole.value;
                            localStorage.setItem("azureRole" + voiceType, preSetVoiceRole.value);
                        }
                        preSetVoiceRole.dispatchEvent(new Event("change"));
                    } else {
                        azureRole[voiceType] = void 0;
                        localStorage.removeItem("azureRole" + voiceType);
                    }
                    preSetVoiceRole.style.display = voiceRoles ? "block" : "none";
                    preSetVoiceRole.previousElementSibling.style.display = voiceRoles ? "block" : "none";
                    if (voiceStyles) {
                        preSetVoiceStyle.innerHTML = "";
                        voiceStyles.forEach((style, i) => {
                            let option = document.createElement("option");
                            option.value = style;
                            option.text = style;
                            preSetVoiceStyle.options.add(option);
                        });
                        let localStyle = localStorage.getItem("azureStyle" + voiceType);
                        if (localStyle && voiceStyles.indexOf(localStyle) !== -1) {
                            preSetVoiceStyle.value = localStyle;
                            azureStyle[voiceType] = localStyle;
                        } else {
                            preSetVoiceStyle.selectedIndex = 0;
                            azureStyle[voiceType] = voiceStyles[0];
                        }
                        preSetVoiceStyle.onchange = () => {
                            azureStyle[voiceType] = preSetVoiceStyle.value;
                            localStorage.setItem("azureStyle" + voiceType, preSetVoiceStyle.value)
                        }
                        preSetVoiceStyle.dispatchEvent(new Event("change"));
                    } else {
                        azureStyle[voiceType] = void 0;
                        localStorage.removeItem("azureStyle" + voiceType);
                    }
                    preSetVoiceStyle.style.display = voiceStyles ? "block" : "none";
                    preSetVoiceStyle.previousElementSibling.style.display = voiceStyles ? "block" : "none";
                } else {
                    document.getElementById("azureExtra").style.display = "none";
                    azureRole[voiceType] = void 0;
                    localStorage.removeItem("azureRole" + voiceType);
                    azureStyle[voiceType] = void 0;
                    localStorage.removeItem("azureStyle" + voiceType);
                }
            };
            // 根据参数 type 加载另一种语音，并从本地存储中读取 Azure 语音服务的风格和角色信息（如果存在）
            const loadAnother = (type) => {
                type = type ^ 1;
                let localVoice = localStorage.getItem("voice" + type);
                if (localVoice) {
                    let localIndex = voices.findIndex(item => {
                        return item.name === localVoice
                    });
                    if (localIndex === -1) localIndex = 0;
                    voiceRole[type] = voices[localIndex];
                } else {
                    voiceRole[type] = voices[0];
                }
                if (existVoice === 3) {
                    let localStyle = localStorage.getItem("azureStyle" + type);
                    azureStyle[type] = localStyle ? localStyle : void 0;
                    let localRole = localStorage.getItem("azureRole" + type);
                    azureRole[type] = localRole ? localRole : void 0;
                }
            }
            // 当语音类型发生改变时，根据本地存储的语音信息，更新当前语音的角色，并将下拉框的选中项更新为本地存储的语音名称。
            //如果本地存储中没有语音信息，则默认使用微软小艾的语音。最后触发下拉框的 change 事件。
            const voiceChange = () => {
                let localVoice = localStorage.getItem("voice" + voiceType);
                if (localVoice) {
                    let localIndex = voices.findIndex(item => {
                        return item.name === localVoice
                    });
                    if (localIndex === -1) localIndex = 0;
                    voiceRole[voiceType] = voices[localIndex];
                    voicesEle.value = localIndex;
                } else {
                    //首次进来 使用微软xiaoyi
                    voiceRole[1] = voices[4];
                }
                voicesEle.dispatchEvent(new Event("change"));
            }
            // 调用 voiceChange 函数和 loadAnother 函数，分别更新当前语音的角色和加载另一种语音。
            voiceChange();
            loadAnother(voiceType);
            //读取本地存储中的音量信息，如果存在则更新音量值和滑动条的样式，如果不存在则触发滑动条的 input 事件
            let volumeEle = document.getElementById("voiceVolume");
            let localVolume = localStorage.getItem("voiceVolume0");
            voiceVolume[0] = parseFloat(localVolume ? localVolume : volumeEle.value);
            const voiceVolumeChange = () => {
                let localVolume = localStorage.getItem("voiceVolume" + voiceType);
                if (localVolume) {
                    voiceVolume[voiceType] = parseFloat(localVolume);
                    volumeEle.value = localVolume;
                    volumeEle.style.backgroundSize = (volumeEle.value - volumeEle.min) * 100 / (volumeEle.max -
                        volumeEle.min) + "% 100%";
                } else {
                    volumeEle.dispatchEvent(new Event("input"));
                }
            }
            //声音音量输入事件
            volumeEle.oninput = () => {
                volumeEle.style.backgroundSize = (volumeEle.value - volumeEle.min) * 100 / (volumeEle.max -
                    volumeEle.min) + "% 100%";
                voiceVolume[voiceType] = parseFloat(volumeEle.value);
                localStorage.setItem("voiceVolume" + voiceType, volumeEle.value);
            }
            voiceVolumeChange();
            let rateEle = document.getElementById("voiceRate");
            let localRate = localStorage.getItem("voiceRate0");
            voiceRate[0] = parseFloat(localRate ? localRate : rateEle.value);
            //音速设置模块
            const voiceRateChange = () => {
                let localRate = localStorage.getItem("voiceRate" + voiceType);
                if (localRate) {
                    voiceRate[voiceType] = parseFloat(localRate);
                    rateEle.value = localRate;
                    rateEle.style.backgroundSize = (rateEle.value - rateEle.min) * 100 / (rateEle.max - rateEle
                        .min) + "% 100%";
                } else {
                    rateEle.dispatchEvent(new Event("input"));
                }
            }
            //音速设置输入
            rateEle.oninput = () => {
                rateEle.style.backgroundSize = (rateEle.value - rateEle.min) * 100 / (rateEle.max - rateEle
                    .min) + "% 100%";
                voiceRate[voiceType] = parseFloat(rateEle.value);
                localStorage.setItem("voiceRate" + voiceType, rateEle.value);
            }
            voiceRateChange();

            //音调设置模块
            let pitchEle = document.getElementById("voicePitch");
            let localPitch = localStorage.getItem("voicePitch0");
            voicePitch[0] = parseFloat(localPitch ? localPitch : pitchEle.value);
            const voicePitchChange = () => {
                let localPitch = localStorage.getItem("voicePitch" + voiceType);
                if (localPitch) {
                    voicePitch[voiceType] = parseFloat(localPitch);
                    pitchEle.value = localPitch;
                    pitchEle.style.backgroundSize = (pitchEle.value - pitchEle.min) * 100 / (pitchEle.max -
                        pitchEle.min) + "% 100%";
                } else {
                    pitchEle.dispatchEvent(new Event("input"));
                }
            }
            pitchEle.oninput = () => {
                pitchEle.style.backgroundSize = (pitchEle.value - pitchEle.min) * 100 / (pitchEle.max - pitchEle
                    .min) + "% 100%";
                voicePitch[voiceType] = parseFloat(pitchEle.value);
                localStorage.setItem("voicePitch" + voiceType, pitchEle.value);
            }
            //调用音调设置函数
            voicePitchChange();
            // 给语音类型切换的按钮绑定点击事件，当点击切换按钮时，根据点击的按钮的 data-type 属性，判断用户选择的语音类型是否与当前语音类型不同，
            //如果不同则更新语音类型、选中状态、语音角色、音量、语速和音调。
            document.getElementById("voiceTypes").onclick = (ev) => {
                let type = ev.target.dataset.type;
                if (type !== void 0) {
                    type = parseInt(type);
                    if (type != voiceType) {
                        voiceType = type;
                        ev.target.classList.add("selVoiceType");
                        ev.target.parentElement.children[type ^ 1].classList.remove("selVoiceType");
                        voiceChange();
                        voiceVolumeChange();
                        voiceRateChange();
                        voicePitchChange();
                    }
                };
            };
            //连续朗读
            const contVoiceEle = document.getElementById("enableContVoice");
            let localCont = localStorage.getItem("enableContVoice");
            if (localCont) {
                enableContVoice = localCont === "true";
                contVoiceEle.checked = enableContVoice;
            }
            contVoiceEle.onchange = () => {
                enableContVoice = contVoiceEle.checked;
                localStorage.setItem("enableContVoice", enableContVoice);
            }
            contVoiceEle.dispatchEvent(new Event("change"));
            const autoVoiceEle = document.getElementById("enableAutoVoice");
            //自动朗读
            let localAuto = localStorage.getItem("enableAutoVoice");
            if (localAuto) {
                enableAutoVoice = localAuto === "true";
                autoVoiceEle.checked = enableAutoVoice;
            }
            autoVoiceEle.onchange = () => {
                enableAutoVoice = autoVoiceEle.checked;
                localStorage.setItem("enableAutoVoice", enableAutoVoice);
            }
            autoVoiceEle.dispatchEvent(new Event("change"));
        };
        //语音设置面板分发change时间
        speechServiceEle.dispatchEvent(new Event("change"));
    </script>
    <script src="//cdn.staticfile.org/markdown-it/13.0.1/markdown-it.min.js"></script>
    <script src="//cdn.staticfile.org/highlight.js/11.7.0/highlight.min.js"></script>
    <script src="//cdn.staticfile.org/KaTeX/0.16.4/katex.min.js"></script>
    <script src="//npm.elemecdn.com/markdown-it-texmath@1.0.0/texmath.js"></script>
    <script src="//npm.elemecdn.com/markdown-it-link-attributes@4.0.1/dist/markdown-it-link-attributes.min.js"></script>
    <script>
        const API_URL = "v1/chat/completions";
        let loading = false;
        //预设角色数据
        let presetRoleData = {
            "normal": "你是一个乐于助人的助手，尽量简明扼要地回答",
            "cat": "你是一个可爱的猫娘，每句话结尾都要带个'喵'",
            "emoji": "你的性格很活泼，每句话中都要有至少一个emoji图标",
            "image": "当你需要发送图片的时候，请用 markdown 语言生成，不要反斜线，不要代码框，需要使用 unsplash API时，遵循一下格式， https://source.unsplash.com/960x640/? ＜英文关键词＞"
        };
        let modelVersion; // 模型版本
        let apiHost; // api反代地址
        let customAPIKey; // 自定义apiKey
        let systemRole; // 自定义系统角色
        let roleNature; // 角色性格
        let roleTemp; // 回答质量
        let enableCont; // 是否开启连续对话，默认开启，对话包含上下文信息。
        let enableLongReply; // 是否开启长回复，默认关闭，开启可能导致api费用增加。
        let longReplyFlag;
        let textSpeed; // 打字机速度，越小越快
        let voiceIns; // Audio or SpeechSynthesisUtterance
        //Audio API 是 HTML5 中的一个音频 API，可以通过 JavaScript 控制音频的播放、暂停、停止等操作。
        //在语音合成中，可以使用 Audio API 将合成的语音转换成音频文件，然后播放出来。
        //但是，使用 Audio API 无法对语音进行实时的控制，比如修改语速、音调等参数。
        //SpeechSynthesisUtterance 是 Web Speech API 中的一个接口，用于语音合成，可以通过 JavaScript 创建一个 SpeechSynthesisUtterance 实例，
        //设置语音的文本、语速、音调等参数，然后通过 SpeechSynthesis API 进行播放。SpeechSynthesisUtterance 支持实时控制语音的参数，
        //可以根据用户的需求动态调整语速、音调等参数。但是，SpeechSynthesisUtterance 无法将语音转换成音频文件，只能在浏览器中直接播放。
        let supportMSE = !!window.MediaSource; // 是否支持MSE（除了ios应该都支持）
        let voiceMIME = "audio/mpeg";
        //网页滚动条自动滚动到底部。
        // //函数scrollToBottom()的作用是，如果网页滚动条距离底部的距离小于128像素，那么就将网页滚动条滚动到底部。
        const scrollToBottom = () => {
            if (messagsEle.scrollHeight - messagsEle.scrollTop - messagsEle.clientHeight < 128) {
                messagsEle.scrollTo(0, messagsEle.scrollHeight)
            }
        }
        //函数scrollToBottomLoad(ele)的作用是，如果网页滚动条距离底部的距离小于元素ele的高度加上128像素，那么就将网页滚动条滚动到底部
        const scrollToBottomLoad = (ele) => {
            if (messagsEle.scrollHeight - messagsEle.scrollTop - messagsEle.clientHeight < ele.clientHeight + 128) {
                messagsEle.scrollTo(0, messagsEle.scrollHeight)
            }
        }
        //这段代码使用了markdown-it库和highlight.js库，用于将Markdown语法解析为HTML，并进行高亮显示。
        //同时还使用了texmath插件，用于支持LaTeX公式的渲染。最后，使用markdownitLinkAttributes插件，
        //为markdown中使用的链接添加target和rel属性，防止出现安全问题
        const md = markdownit({
            linkify: true, // 识别链接
            highlight: function (str, lang) { // markdown高亮
                try {
                    return hljs.highlightAuto(str).value;
                } catch (e) {}
                return ""; // use external default escaping
            }
        });
        md.use(texmath, {
                engine: katex,
                delimiters: "dollars",
                katexOptions: {
                    macros: {
                        "\\RR": "\\mathbb{R}"
                    }
                }
            })
            .use(markdownitLinkAttributes, {
                attrs: {
                    target: "_blank",
                    rel: "noopener"
                }
            });
        const x = {
            //函数getCodeLang(str)的作用是获取code标签中的语言类型，例如<code class="language-javascript"></code>，则返回"javascript"。
            getCodeLang(str = "") {
                const res = str.match(/ class="language-(.*?)"/);
                return (res && res[1]) || "";
            },
            //函数getFragment(str)的作用是根据传入的字符串生成一个HTML片段，并在其中加入一个class为"u-mdic-copy-code_lang"的span元素用于显示代码的语言类型，如果没有传入字符串则返回一个空字符串
            getFragment(str = "") {
                return str ? `<span class="u-mdic-copy-code_lang">${str}</span>` : "";
            },
        };
        //函数strEncode(str)的作用是把字符串中的特殊字符进行编码，使其在HTML中正确显示。
        const strEncode = (str = "") => {
            if (!str || str.length === 0) return "";
            return str
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/'/g, '\'')
                .replace(/"/g, '&quot;');
        };
        //函数getCodeLangFragment(oriStr)的作用是获取oriStr字符串中的代码块的语言类型，并把获取到的语言类型包装在一个HTML片段中返回。
        const getCodeLangFragment = (oriStr = "") => {
            return x.getFragment(x.getCodeLang(oriStr));
        };
        //函数copyClickCode(ele)的作用是，在点击ele元素时，将ele元素的data-mdic-content内容复制到剪贴板中，并显示
        const copyClickCode = (ele) => {
            const input = document.createElement("textarea");
            input.value = ele.dataset.mdicContent;
            const nDom = ele.previousElementSibling;
            const nDelay = ele.dataset.mdicNotifyDelay;
            const cDom = nDom.previousElementSibling;
            document.body.appendChild(input);
            input.select();
            input.setSelectionRange(0, 9999);
            document.execCommand("copy");
            document.body.removeChild(input);
            if (nDom.style.display === "none") {
                nDom.style.display = "block";
                cDom && (cDom.style.display = "none");
                setTimeout(() => {
                    nDom.style.display = "none";
                    cDom && (cDom.style.display = "block");
                }, nDelay);
            }
        };
        //函数copyClickMd(idx)的作用是，在点击具有指定索引idx的元素时，
        //将data数组中对应index的元素的content内容复制到剪贴板中。复制的过程是先创建一个textarea元素，
        //把content内容赋值给它的value属性，然后将它添加到document中，对它进行选中，并执行复制操作，
        //最后再从document中移除它
        const copyClickMd = (idx) => {
            const input = document.createElement("textarea");
            input.value = data[idx].content;
            document.body.appendChild(input);
            input.select();
            input.setSelectionRange(0, 9999);
            document.execCommand("copy");
            document.body.removeChild(input);
        }
        //用于对渲染函数render进行增强，在代码块上添加一个复制按钮
        //enhanceCode函数使用了闭包的方式返回一个函数，这个函数会在调用时根据传入的tokens和idx参数来获取对应位置的代码块的内容，
        //并对这个内容进行编码。然后根据options中提供的配置信息，生成一个HTML片段，包含了复制按钮、成功提示等元素，将这个HTML片段插入到代码块末尾，
        //返回增强后的HTML代码。内部使用了其他函数strEncode、getCodeLangFragment和copyClickCode来实现对字符串的编码、获取代码块语言类型和复制元素内容的功能
        const enhanceCode = (render, options = {}) => (...args) => {
            /* args = [tokens, idx, options, env, slf] */
            const {
                btnText = "复制代码", // button text
                    successText = "复制成功", // copy-success text
                    successTextDelay = 2000, // successText show time [ms]
                    showCodeLanguage = true, // false | show code language
            } = options;
            const [tokens = {}, idx = 0] = args;
            const cont = strEncode(tokens[idx].content || "");
            const originResult = render.apply(this, args);
            const langFrag = showCodeLanguage ? getCodeLangFragment(originResult) : "";
            const tpls = [
                '<div class="m-mdic-copy-wrapper">',
                `${langFrag}`,
                `<div class="u-mdic-copy-notify" style="display:none;">${successText}</div>`,
                '<button ',
                'class="u-mdic-copy-btn j-mdic-copy-btn" ',
                `data-mdic-content="${cont}" `,
                `data-mdic-notify-delay="${successTextDelay}" `,
                `onclick="copyClickCode(this)">${btnText}</button>`,
                '</div>',
            ];
            const LAST_TAG = "</pre>";
            const newResult = originResult.replace(LAST_TAG, `${tpls.join("")}${LAST_TAG}`);
            return newResult;
        };
        //这段代码扩展了markdown-it的渲染规则。其中：
        //codeBlockRender和fenceRender用于获取渲染代码块的规则，即其原有渲染方式；
        //调用enhanceCode函数对渲染代码块的规则codeBlockRender和fenceRender进行增强；
        //对于markdown中的图片，添加了两个事件处理程序onload和onerror，并将这张图片的高度作为参数传入scrollToBottomLoad函数，用于在图片加载完成或加载失败时，自动滚动到页面底部。
        //最终返回的是一个渲染的Token，它包含了图片的一些元数据和相关处理。
        const codeBlockRender = md.renderer.rules.code_block;
        const fenceRender = md.renderer.rules.fence;
        md.renderer.rules.code_block = enhanceCode(codeBlockRender);
        md.renderer.rules.fence = enhanceCode(fenceRender);
        md.renderer.rules.image = function (tokens, idx, options, env, slf) {
            let token = tokens[idx];
            token.attrs[token.attrIndex("alt")][1] = slf.renderInlineAsText(token.children, options, env);
            token.attrSet("onload",
                "scrollToBottomLoad(this);this.removeAttribute('onload');this.removeAttribute('onerror')");
            token.attrSet("onerror",
                "scrollToBottomLoad(this);this.removeAttribute('onload');this.removeAttribute('onerror')");
            return slf.renderToken(tokens, idx, options)
        }
        //这段代码是一个事件处理函数mdOptionEvent，用于处理用户在消息窗口中点击了一些预设的操作按钮之后的行为，例如：播放语音、重新加载内容、复制消息、删除消息、下载音频等。
        //当用户在某个特定的元素上点击时，会根据元素中的data-id属性来确定用户想要进行什么操作。例如，当data-id属性为"voiceMd"时，表示用户要播放语音，此时会根据元素的样式来判断当前是在暂停或者播放状态，然后分别执行暂停或继续播放操作。
        //除此之外，该事件处理函数还涉及了一些跟数据和DOM操作相关的行为，如删除节点、修改DOM属性、更新数组等，主要目的是根据用户的行为修改页面的呈现和数据的结构。
        const mdOptionEvent = function (ev) {
            let id = ev.target.dataset.id;
            if (id) {
                let parent = ev.target.parentElement;
                let idxEle = parent.parentElement;
                let idx = Array.prototype.indexOf.call(chatlog.children, this.parentElement);
                if (id === "voiceMd") {
                    let className = parent.className;
                    if (className == "readyVoice") {
                        if (chatlog.children[idx].dataset.loading !== "true") {
                            idx = systemRole ? idx + 1 : idx;
                            speechEvent(idx);
                        }
                    } else if (className == "pauseVoice") {
                        if (existVoice >= 2) {
                            if (voiceIns) voiceIns.pause();
                        } else {
                            speechSynthesis.pause();
                        }
                        parent.className = "resumeVoice";
                    } else {
                        if (existVoice >= 2) {
                            if (voiceIns) voiceIns.play();
                        } else {
                            speechSynthesis.resume();
                        }
                        parent.className = "pauseVoice";
                    }
                } else if (id === "refreshMd") {
                    if (!loading && chatlog.children[idx].dataset.loading !== "true") {
                        let className = parent.className;
                        if (className == "refreshReq") {
                            chatlog.children[idx].children[0].innerHTML = "<br />";
                            chatlog.children[idx].dataset.loading = true;
                            idx = systemRole ? idx + 1 : idx;
                            data[idx].content = "";
                            if (idx === currentVoiceIdx) {
                                endSpeak()
                            };
                            loadAction(true);
                            refreshIdx = idx;
                            streamGen();
                        } else {
                            chatlog.children[idx].dataset.loading = true;
                            idx = systemRole ? idx + 1 : idx;
                            progressData = data[idx].content;
                            loadAction(true);
                            refreshIdx = idx;
                            streamGen(true);
                        }
                    }
                } else if (id === "copyMd") {
                    idxEle.classList.add("moreOptionHidden");
                    idx = systemRole ? idx + 1 : idx;
                    copyClickMd(idx);
                    notyf.success("复制成功");
                } else if (id === "delMd") {
                    idxEle.classList.add("moreOptionHidden");
                    if (!loading) {
                        if (confirmAction("是否删除此消息?")) {
                            if (currentVoiceIdx) {
                                if (currentVoiceIdx === idx) {
                                    endSpeak()
                                } else if (currentVoiceIdx > idx) {
                                    currentVoiceIdx -= 1
                                }
                            }
                            chatlog.removeChild(chatlog.children[idx]);
                            idx = systemRole ? idx + 1 : idx;
                            data.splice(idx, 1);
                            updateChats();
                        }
                    }
                } else if (id === "downAudio") {
                    idxEle.classList.add("moreOptionHidden");
                    if (chatlog.children[idx].dataset.loading !== "true") {
                        idx = systemRole ? idx + 1 : idx;
                        //聊天框里面的上下文按钮下载音频
                        downloadAudio(idx);
                    }
                }
            }
        }
        //moreOption函数用于控制"更多选项"区域的显隐性，在用户将鼠标移动到包含特定样式的元素上时，会调用此函数来显示当前元素的更多选项。
        const moreOption = (ele) => {
            ele.classList.remove("moreOptionHidden");
        }
        //formatMdEle函数用于生成特定样式的元素，并表现出消息内容及其相关操作的所有UI细节。它在元素上附加了一个realMd子元素，用于展示实际的markdown内容，
        //并添加一个mdOption子元素，它包含用户可以执行的操作和控制更多选项区域的逻辑功能。
        //根据不同的元素样式，mdOption会用不同的内容进行填充。当界面上存在语音时，用户将可以看到朗读语音的按钮，而在其他的情况下，它们将看到复制
        const formatMdEle = (ele) => {
            let realMd = document.createElement("div");
            realMd.className = ele.className === "request" ? "requestBody" : "markdown-body";
            ele.appendChild(realMd);
            let mdOption = document.createElement("div");
            mdOption.className = "mdOption";
            ele.appendChild(mdOption);
            if (ele.className !== "request") {
                mdOption.innerHTML = `<div class="refreshReq">
                    <svg data-id="refreshMd" width="16" height="16" role="img"><title>刷新</title><use xlink:href="#refreshIcon" /></svg>
                    <svg data-id="refreshMd" width="16" height="16" role="img"><title>继续</title><use xlink:href="#halfRefIcon" /></svg>
                    </div>`
            }
            let optionWidth = existVoice >= 2 ? "96px" : "63px";
            mdOption.innerHTML += `<div class="moreOption" onmouseenter="moreOption(this)">
            <svg class="optionTrigger" width="16" height="16" role="img"><title>选项</title><use xlink:href="#optionIcon" /></svg>
            <div class="optionItems" style="width:${optionWidth};left:-${optionWidth}">` + (existVoice >= 2 ? `<div data-id="downAudio" class="optionItem" title="下载语音">
                <svg width="20" height="20"><use xlink:href="#downAudioIcon" /></svg>
            </div>` : "") + `<div data-id="delMd" class="optionItem" title="删除">
                <svg width="20" height="20"><use xlink:href="#delIcon" /></svg>
            </div>
            <div data-id="copyMd" class="optionItem" title="复制">
                <svg width="20" height="20"><use xlink:href="#copyIcon" /></svg>
            </div></div></div>`;
            if (existVoice) {
                mdOption.innerHTML += `<div id="pronMd" class="readyVoice">
                <svg width="16" height="16" data-id="voiceMd" role="img"><title>朗读</title><use xlink:href="#readyVoiceIcon" /></svg>
                <svg width="16" height="16" data-id="voiceMd" role="img"><title>暂停</title><use xlink:href="#pauseVoiceIcon" /></svg>
                <svg width="16" height="16" data-id="voiceMd" role="img"><title>继续</title><use xlink:href="#resumeVoiceIcon" /></svg>
                </div>`
            }
            mdOption.onclick = mdOptionEvent;
        }
        //这段代码实现了添加新的聊天元素，并为聊天元素绑定了 onclick 事件。
        //chatEleAdd 函数被调用时，它传入的参数 idx 是聊天数据的索引，用于获取聊天数据。然后，函数创建了一个新的 div 节点 chatEle，
        //给其附加类名 chatLi。接下来，它为 chatEle 设置了内部的 HTML 内容，其中包含了聊天的图标、名称以及编辑和删除聊天的选项。
        //然后将 chatEle 添加到聊天列表的 chatListEle 中，并给它绑定 chatEleEvent 事件。最后，函数返回 chatEle。
        //需要注意的是，代码的前置条件是聊天列表的 HTML 标记必须在代码的其他部分中合适地定义和初始化。此外，聊天的数据也需要通过其他功能进行获取和填充。
        let chatsData = [];
        let activeChatIdx = 0;
        let data;
        const chatEleAdd = (idx) => {
            let chat = chatsData[idx];
            let chatEle = document.createElement("div");
            chatEle.className = "chatLi";
            chatEle.innerHTML =
                `<svg width="24" height="24"><use xlink:href="#chatIcon" /></svg>
                <div class="chatName">${chat.name}</div>
                <div class="chatOption"><svg data-type="chatEdit" style="margin-right:4px" width="24" height="24" role="img"><title>编辑</title><use xlink:href="#chatEditIcon" /></svg>
                <svg data-type="chatDel" width="24" height="24" role="img"><title>删除</title><use xlink:href="#delIcon" /></svg></div>`
            chatListEle.appendChild(chatEle);
            chatEle.onclick = chatEleEvent;
            return chatEle;
        };
        //添加新的会话
        const addNewChat = () => {
            let chat = {
                name: "新的会话",
                data: []
            };
            chatsData.push(chat);
            updateChats();
        };
        //这是一个删除会话的函数。首先会弹出确认窗口，如果确认删除，则会执行以下步骤：
        //结束所有会话；
        //如果要删除的是当前激活的会话，根据删除的位置来更新激活的会话；
        //从会话列表中删除该会话的DOM元素；
        //如果删除后没有其他会话，则自动添加一个新的会话；
        //更新本地存储的会话数据；
        //显示激活的会话的聊天记录。
        //其中confirmAction是一个自定义的函数用于弹出确认窗口，endAll是一个自定义的函数用于关闭当前正在编辑的选项卡和输入框。
        const delChat = (idx) => {
            if (confirmAction("是否删除会话?")) {
                endAll();
                if (idx === activeChatIdx) {
                    if (idx - 1 >= 0) {
                        activeChatIdx = idx - 1;
                    } else if (idx === chatsData.length - 1) {
                        activeChatIdx = chatsData.length - 2;
                    }
                }
                chatsData.splice(idx, 1);
                chatListEle.children[idx].remove();
                if (activeChatIdx === -1) {
                    addNewChat();
                    activeChatIdx = 0;
                    chatEleAdd(activeChatIdx);
                }
                updateChats();
                activeChat();
            }
        };
        //这是一个结束编辑事件的函数。主要作用是在编辑会话名称时，当用户点击页面上除编辑框以外的其他区域时，自动保存编辑内容并关闭编辑框
        //具体流程为：
        //获取当前正在编辑的选项卡的输入框；
        //判断是否点击的是该输入框以外的其他区域；
        //如果是，则更新该会话的名称，并在对应的DOM元素上更新名称，同时移除已经不需要的编辑框；
        //更新本地存储的会话数据；
        //移除事件监听器，完成编辑事件。
        const endEditEvent = (ev) => {
            let activeEle = document.getElementById("activeChatEdit")
            if (!activeEle.contains(ev.target)) {
                let ele = chatListEle.children[activeChatIdx];
                chatsData[activeChatIdx].name = activeEle.value;
                ele.children[1].textContent = activeEle.value;
                ele.lastElementChild.remove();
                updateChats();
                document.body.removeEventListener("mousedown", endEditEvent, true);
            }
        };
        //这是一个开始编辑会话名称的函数。当用户点击了编辑图标时会调用该函数，在对应的会话列表元素上生成一个输入框，
        //让用户可以直接在界面上编辑该会话的名称
        const toEditChatName = (idx) => {
            let inputEle = document.createElement("input");
            inputEle.id = "activeChatEdit";
            inputEle.value = chatsData[idx].name;
            chatListEle.children[idx].appendChild(inputEle);
            inputEle.focus();
            document.body.addEventListener("mousedown", endEditEvent, true);
        };
        //这是一个处理会话列表元素的点击事件的函数。主要判断点击的元素是会话列表元素本身、编辑图标还是删除图标，并分别进行响应。
        //阻止默认的点击事件，并停止事件冒泡，以避免对页面的其他部分造成影响；
        //获取当前点击的会话列表元素在列表中的索引值；
        //如果点击的是会话列表元素本身，判断是否为当前激活的会话，如果不是，则结束所有的聊天编辑，并激活该会话，并关闭导航菜单；
        //如果点击的是编辑图标，则转到编辑该会话名称的函数；
        //如果点击的是删除图标，则调用删除会话的函数进行相关操作。
        const chatEleEvent = function (ev) {
            ev.preventDefault();
            ev.stopPropagation();
            let idx = Array.prototype.indexOf.call(chatListEle.children, this);
            if (ev.target.className === "chatLi") {
                if (activeChatIdx !== idx) {
                    endAll();
                    activeChatIdx = idx;
                    activeChat();
                }
                document.body.classList.remove("show-nav");
            } else if (ev.target.dataset.type === "chatEdit") {
                toEditChatName(idx);
            } else if (ev.target.dataset.type === "chatDel") {
                delChat(idx);
            }
        };
        //这是一个更新会话数据的函数，将当前的会话数据以JSON字符串的形式保存到本地存储中。
        const updateChats = () => {
            localStorage.setItem("chats", JSON.stringify(chatsData));
        };
        //这是一个用于创建对话框元素的函数，根据传入的className参数生成相应的对话框元素。
        const createConvEle = (className) => {
            let div = document.createElement("div");
            div.className = className;
            chatlog.appendChild(div);
            formatMdEle(div);
            return div;
        }
        //这是一个激活当前会话的函数。在聊天对话框中，如果用户点击某个会话的列表元素，则会激活该会话的聊天记录，
        //该函数就是用于激活对应会话的。
        const activeChat = () => {
            data = chatsData[activeChatIdx]["data"];
            Array.from(document.getElementsByClassName("activeChatLi")).forEach(item => {
                item.classList.remove("activeChatLi");
            });
            chatListEle.children[activeChatIdx].classList.add("activeChatLi");
            if (data[0] && data[0].role === "system") {
                systemRole = data[0].content;
                systemEle.value = systemRole;
                localStorage.setItem("system", systemRole);
            } else {
                systemRole = "";
                systemEle.value = "";
                localStorage.setItem("system", systemRole);
            }
            chatlog.innerHTML = "";
            if (systemRole ? data.length - 1 : data.length) {
                let firstIdx = systemRole ? 1 : 0;
                for (let i = firstIdx; i < data.length; i++) {
                    if (data[i].role === "user") {
                        createConvEle("request").children[0].textContent = data[i].content;
                    } else {
                        createConvEle("response").children[0].innerHTML = md.render(data[i].content) || "<br />";
                    }
                }
            }
            localStorage.setItem("activeChatIdx", activeChatIdx);
        };
        //这是一个点击“新建会话”按钮时的回调函数，主要用于新建一个会话并激活它。
        //结束所有的聊天编辑和输入，关闭导航菜单；
        //新建一个会话并将其添加到会话数据中；将新建的
        newChatEle.onclick = () => {
            endAll();
            addNewChat();
            activeChatIdx = chatsData.length - 1;
            chatEleAdd(activeChatIdx);
            activeChat();
        };
        //初始化聊天数据，从本地存储中获取聊天信息，如果有则解析，如果没有则创建新的聊天。
        const initChats = () => {
            let localChats = localStorage.getItem("chats");
            let localChatIdx = localStorage.getItem("activeChatIdx")
            activeChatIdx = (localChatIdx && parseInt(localChatIdx)) || 0;
            if (localChats) {
                chatsData = JSON.parse(localChats);
                for (let i = 0; i < chatsData.length; i++) {
                    chatEleAdd(i);
                }
            } else {
                addNewChat();
                chatEleAdd(activeChatIdx);
            }
        };
        initChats();
        activeChat();
        //当id为"exportChat"的元素被点击时，将聊天数据以json格式导出并下载。
        document.getElementById("exportChat").onclick = () => {
            if (loading) {
                stopLoading();
            }
            let blob = new Blob([JSON.stringify(chatsData, null, 2)], {
                type: "application/json"
            });
            let date = new Date();
            let fileName = "chats-" + date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() +
                ".json";
            downBlob(blob, fileName);
        };
        //将Blob对象转化为文本格式。
        const blobToText = (blob) => {
            return new Promise((res, rej) => {
                let reader = new FileReader();
                reader.readAsText(blob);
                reader.onload = () => {
                    res(reader.result);
                }
                reader.onerror = (error) => {
                    rej(error);
                }
            })
        };
        //当id为"importChatInput"的文件选择器的文件改变时，将选择的文件导入解析，并将其添加到聊天数据中
        //获取选择的文件。
        //利用blobToText()函数将Blob对象转为文本。
        //使用JSON.parse()函数将文本格式转为JSON对象。
        //检查JSON对象的每一项是否存在"name"和"data"字段，如果存在就认为格式检查通过。
        //如果检查通过，则将解析后的数据加入到原有的chatsData数组中，并调用chatEleAdd()函数将其添加到页面中。此处循环添加聊天数据的操作可以通过shift()方法实现。
        //更新聊天列表。
        //如果检查未通过，则抛出异常。
        //如果解析或检查出现错误，弹出提示信息。
        //将文件选择器的值清空，以便下次选择文件。
        document.getElementById("importChatInput").onchange = function () {
            let file = this.files[0];
            blobToText(file).then(text => {
                try {
                    let json = JSON.parse(text);
                    let checked = json.every(item => {
                        return item.name !== void 0 && item.data !== void 0;
                    });
                    if (checked) {
                        while (json.length) {
                            chatsData.push(json.shift());
                            chatEleAdd(chatsData.length - 1);
                        }
                        updateChats();
                    } else {
                        throw new Error("格式检查不通过")
                    }
                } catch (e) {
                    notyf.error("导入失败，请检查文件是否正确！")
                }
                this.value = "";
            })
        };
        //当id为"clearChat"的元素被点击时，弹出一个确认框，如果确认清空所有聊天，则清空所有聊天数据并重新添加一个新的聊天
        document.getElementById("clearChat").onclick = () => {
            if (confirmAction("是否清空所有会话?")) {
                chatsData.length = 0;
                chatListEle.innerHTML = "";
                endAll();
                addNewChat();
                activeChatIdx = 0;
                chatEleAdd(activeChatIdx);
                updateChats();
                activeChat();
            }
        };
        //结束所有未结束的聊天
        const endAll = () => {
            endSpeak();
            if (loading) stopLoading();
        };
        //初始化设置信息，并将设置项与本地存储中的信息同步
        const initSetting = () => {
            //获取页面中对应元素，同时从本地存储中获取对应的配置
            const modelEle = document.getElementById("preSetModel");
            let localModel = localStorage.getItem("modelVersion");
            if (localModel) {
                modelVersion = localModel;
                modelEle.value = localModel;
            }
            modelEle.onchange = () => {
                modelVersion = modelEle.value;
                localStorage.setItem("modelVersion", modelVersion);
            }
            //为设置项设置onchange和oninput事件处理函数，以便进行更新配置和保存配置到本地存储。
            modelEle.dispatchEvent(new Event("change"));
            const apiHostEle = document.getElementById("apiHostInput");
            let localApiHost = localStorage.getItem("APIHost");
            if (localApiHost) {
                apiHost = localApiHost;
                apiHostEle.value = localApiHost;
            }
            //当onchange和oninput事件被触发时，相应的更新配置和保存配置到本地存储。
            apiHostEle.onchange = () => {
                apiHost = apiHostEle.value;
                if (apiHost.length && !apiHost.endsWith("/")) {
                    apiHost += "/";
                    apiHostEle.value = apiHost;
                }
                localStorage.setItem("APIHost", apiHost);
            }
            apiHostEle.dispatchEvent(new Event("change"));
            const keyEle = document.getElementById("keyInput");
            let localKey = localStorage.getItem("APIKey");
            if (localKey) {
                customAPIKey = localKey;
                keyEle.value = localKey;
            }
            keyEle.onchange = () => {
                customAPIKey = keyEle.value;
                localStorage.setItem("APIKey", customAPIKey);
            }
            keyEle.dispatchEvent(new Event("change"));
            if (systemRole === void 0) {
                let localSystem = localStorage.getItem("system");
                if (localSystem) {
                    systemRole = localSystem;
                    systemEle.value = localSystem;
                    data.unshift({
                        role: "system",
                        content: systemRole
                    });
                    updateChats();
                } else {
                    systemRole = systemEle.value;
                }
            }
            //初始化时将系统角色信息添加到聊天记录中，并更新聊天列表。
            systemEle.onchange = () => {
                systemRole = systemEle.value;
                localStorage.setItem("system", systemRole);
                if (systemRole) {
                    if (data[0] && data[0].role === "system") {
                        data[0].content = systemRole;
                    } else {
                        data.unshift({
                            role: "system",
                            content: systemRole
                        });
                    }
                } else if (data[0] && data[0].role === "system") {
                    data.shift();
                }
                updateChats();
            }
            //如果用户修改了系统角色，则更新聊天记录和聊天列表。
            const preEle = document.getElementById("preSetSystem");
            preEle.onchange = () => {
                let val = preEle.value;
                if (val && presetRoleData[val]) {
                    systemEle.value = presetRoleData[val];
                } else {
                    systemEle.value = "";
                }
                systemEle.dispatchEvent(new Event("change"));
                systemEle.focus();
            }
            //将设置项的默认值与本地存储中的值进行同步，以实现持久化
            const topEle = document.getElementById("top_p");
            let localTop = localStorage.getItem("top_p");
            if (localTop) {
                roleNature = parseFloat(localTop);
                topEle.value = localTop;
            }
            topEle.oninput = () => {
                topEle.style.backgroundSize = (topEle.value - topEle.min) * 100 / (topEle.max - topEle.min) +
                    "% 100%";
                roleNature = parseFloat(topEle.value);
                localStorage.setItem("top_p", topEle.value);
            }
            topEle.dispatchEvent(new Event("input"));
            const tempEle = document.getElementById("temp");
            let localTemp = localStorage.getItem("temp");
            if (localTemp) {
                roleTemp = parseFloat(localTemp);
                tempEle.value = localTemp;
            }
            tempEle.oninput = () => {
                tempEle.style.backgroundSize = (tempEle.value - tempEle.min) * 100 / (tempEle.max - tempEle
                    .min) + "% 100%";
                roleTemp = parseFloat(tempEle.value);
                localStorage.setItem("temp", tempEle.value);
            }
            tempEle.dispatchEvent(new Event("input"));
            const speedEle = document.getElementById("textSpeed");
            let localSpeed = localStorage.getItem("textSpeed");
            if (localSpeed) {
                textSpeed = parseFloat(speedEle.min) + (speedEle.max - localSpeed);
                speedEle.value = localSpeed;
            }
            speedEle.oninput = () => {
                speedEle.style.backgroundSize = (speedEle.value - speedEle.min) * 100 / (speedEle.max - speedEle
                    .min) + "% 100%";
                textSpeed = parseFloat(speedEle.min) + (speedEle.max - speedEle.value);
                localStorage.setItem("textSpeed", speedEle.value);
            }
            speedEle.dispatchEvent(new Event("input"));
            const contEle = document.getElementById("enableCont");
            let localCont = localStorage.getItem("enableCont");
            if (localCont) {
                enableCont = localCont === "true";
                contEle.checked = enableCont;
            }
            contEle.onchange = () => {
                enableCont = contEle.checked;
                localStorage.setItem("enableCont", enableCont);
            }
            contEle.dispatchEvent(new Event("change"));
            const longEle = document.getElementById("enableLongReply");
            let localLong = localStorage.getItem("enableLongReply");
            if (localLong) {
                enableLongReply = localLong === "true";
                longEle.checked = enableLongReply;
            }
            longEle.onchange = () => {
                enableLongReply = longEle.checked;
                localStorage.setItem("enableLongReply", enableLongReply);
            }
            longEle.dispatchEvent(new Event("change"));
        };
        initSetting();
        //初始化完成 隐藏加载条
        document.getElementById("loadMask").style.display = "none";
        const closeEvent = (ev) => {
            if (settingEle.contains(ev.target)) return;
            if (!dialogEle.contains(ev.target)) {
                dialogEle.style.display = "none";
                document.removeEventListener("mousedown", closeEvent, true);
                settingEle.classList.remove("showSetting");
            }
        }
        //当id为"setting"的元素被鼠标点击时，如果设置面板当前处于显示状态，则隐藏设置面板，否则显示设置面板。
        settingEle.onmousedown = () => {
            //获取id为“dialog”的元素，根据其当前的显示状态，决定是否改变其显示状态。
            dialogEle.style.display = dialogEle.style.display === "block" ? "none" : "block";
            if (dialogEle.style.display === "block") {
                //如果显示设置面板，注册window全局mousedown事件监听器。
                document.addEventListener("mousedown", closeEvent, true);
                settingEle.classList.add("showSetting");
            } else {
                //如果隐藏设置面板，删除window全局mousedown事件监听器。
                document.removeEventListener("mousedown", closeEvent, true);
                //添加或删除class“showSetting”，以控制设置面板的显示和隐藏。
                settingEle.classList.remove("showSetting");
            }
        }
        //保存定时器ID，用于清除定时器。
        let delayId;
        ////打字机时间间隔
        const delay = () => {
            //返回Promise对象，以便进行异步处理
            return new Promise((resolve) => delayId = setTimeout(resolve, textSpeed));
        }
        //生成UUID
        const uuidv4 = () => {
            let uuid = ([1e7] + 1e3 + 4e3 + 8e3 + 1e11).replace(/[018]/g, c =>
                (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
            );
            return existVoice === 3 ? uuid.toUpperCase() : uuid;
        }
        //获取当前时间。
        const getTime = () => {
            //判断existVoice是否等于3，如果等于3则表示当前正在录制语音，则使用toISOString()函数获取当前时间并返回。
            return existVoice === 3 ? new Date().toISOString() : new Date().toString();
        }
        //获取WebSocket连接前缀head
        //定义变量osPlatform，并赋初值。
        //如果当前是在浏览器中运行，则值为"Browser/"加上当前操作系统平台。
        //如果当前是在Node.js环境中运行，则值为"Node/"加上当前操作系统平台。
        //将osName和osVersion从navigator对象中获取到。
        //返回WebSokcet连接前缀，包括：
        //请求路径"path:speech.config"。
        //请求ID"X-RequestId: ${requestId}"。
        //请求时间戳"X-Timestamp: ${date}"。
        //请求类型"Content-Type: application/json"。
        //请求内容{"context":{"system":{"name":"SpeechSDK","version":"1.26.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"${osPlatform}","name":"${osName}","version":"${osVersion}"}}}}。
        const getWSPre = (date, requestId) => {
            let osPlatform = (typeof window !== "undefined") ? "Browser" : "Node";
            osPlatform += "/" + navigator.platform;
            let osName = navigator.userAgent;
            let osVersion = navigator.appVersion;
            return `Path: speech.config\r\nX-RequestId: ${requestId}\r\nX-Timestamp: ${date}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.26.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"${osPlatform}","name":"${osName}","version":"${osVersion}"}}}}`
        }
        //这段函数代码适用于在语音合成技术的开发中，生成符合API要求且能够与后端服务进行数据交互的格式化字符串。
        //同时，当existVoice的值为不同的数值时，返回的字符串可以为后续开发者提供不同的数据参数。
        const getWSAudio = (date, requestId) => {
            return existVoice === 3 ?
                `Path: synthesis.context\r\nX-RequestId: ${requestId}\r\nX-Timestamp: ${date}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-48kbitrate-mono-mp3"}}}` :
                `X-Timestamp:${date}\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":"false","wordBoundaryEnabled":"true"},"outputFormat":"audio-24khz-48kbitrate-mono-mp3"}}}}`
        }
        //这段代码是一个函数，名为getWSText。它接收多个参数，包括日期、请求ID、语言、语音、音量、语速、音高、样式、角色、消息等。返回的是一段符合特定格式要求的字符串。
        const getWSText = (date, requestId, lang, voice, volume, rate, pitch, style, role, msg) => {
            let fmtVolume = volume === 1 ? "+0%" : volume * 100 - 100 + "%";
            let fmtRate = (rate >= 1 ? "+" : "") + (rate * 100 - 100) + "%";
            let fmtPitch = (pitch >= 1 ? "+" : "") + (pitch - 1) + "Hz";
            if (existVoice === 3) {
                let fmtStyle = style ? ` style="${style}"` : "";
                let fmtRole = role ? ` role="${role}"` : "";
                let fmtExpress = fmtStyle + fmtRole;
                return `Path: ssml\r\nX-RequestId: ${requestId}\r\nX-Timestamp: ${date}\r\nContent-Type: application/ssml+xml\r\n\r\n<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='${lang}'><voice name='${voice}'><mstts:express-as${fmtExpress}><prosody pitch='${fmtPitch}' rate='${fmtRate}' volume='${fmtVolume}'>${msg}</prosody></mstts:express-as></voice></speak>`;
            } else {
                return `X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${date}Z\r\nPath:ssml\r\n\r\n<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='${lang}'><voice name='${voice}'><prosody pitch='${fmtPitch}' rate='${fmtRate}' volume='${fmtVolume}'>${msg}</prosody></voice></speak>`;
            }
        }
        //这个函数变量 getAzureWSURL 使用了模板字符串的方式，用 ${} 包含变量替换的方式，拼接出了一个 WebSockets URL。
        //其中 azureRegion 表示 Azure 所在的区域，azureToken 是认证的 Token，%20 表示空格。最终返回的 WebSockets URL 在 wss:// 前缀中包含了 Azure 区域及认证 Token 信息。
        const getAzureWSURL = () => {
            return `wss://${azureRegion}.tts.speech.microsoft.com/cognitiveservices/websocket/v1?Authorization=bearer%20${azureToken}`
        }
        //edge 的wss 地址
        const edgeTTSURL =
            "wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?trustedclienttoken=6A5AA1D4EAFF4E9FB37E23D68491D6F4";
        //该变量用于记录当前正在朗读的句子在聊天窗口中的索引，以便取消其播放状态。
        let currentVoiceIdx;
        //用于重置正在朗读的句子的图标和颜色。函数首先检查 currentVoiceIdx 是否有值
        //如果有，则通过聊天窗口的 children 属性以及记录的索引找到该句子在 DOM 中的位置，并将其预先生成的图标的样式重置为准备播放状态
        const resetSpeakIcon = () => {
            if (currentVoiceIdx !== void 0) {
                chatlog.children[systemRole ? currentVoiceIdx - 1 : currentVoiceIdx].children[1].lastChild
                    .className = "readyVoice";
            }
        }
        //这段代码定义了一个函数 endSpeak，用于停止语音朗读并做一些清理工作
        //首先，调用了之前定义的 resetSpeakIcon 函数，将正在朗读的句子的图标和颜色重置为准备播放状态。
        //然后判断正在朗读的音频源的数量是否超过 2。如果超过 2，说明正在使用 WebSockets 与 Azure 服务进行实时的语音转换，并将结果播放。
        //这种情况下，需要停止语音播放、清理音频资源、关闭 WebSockets 连接，重置一些标志变量，并清空语音转换队列等。
        //否则，直接使用 speechSynthesis 的 cancel() 方法取消朗读。
        //最后，将 currentVoiceIdx 重置为 undefined，表示没有正在朗读的句子。
        const endSpeak = () => {
            resetSpeakIcon();
            if (existVoice >= 2) {
                if (voiceIns) {
                    voiceIns.pause();
                    voiceIns.currentTime = 0;
                    URL.revokeObjectURL(voiceIns.src);
                    voiceIns.removeAttribute("src");
                    voiceIns.onended = voiceIns.onerror = null;
                }
                sourceBuffer = void 0;
                speechPushing = false;
                if (voiceSocket && voiceSocket["pending"]) {
                    voiceSocket.close()
                }
                if (autoVoiceSocket && autoVoiceSocket["pending"]) {
                    autoVoiceSocket.close()
                }
                speechQuene.length = 0;
                autoMediaSource = void 0;
                voiceContentQuene = [];
                voiceEndFlagQuene = [];
                voiceBlobURLQuene = [];
                autoOnlineVoiceFlag = false;
            } else {
                speechSynthesis.cancel();
            }
            currentVoiceIdx = void 0;
            //语音结束播放之后 自动开启新的语音识别
            //debugger
            window.toggleRecEvent();
        }
        //它接收一个用于播放音频的实例 ( ${voiceIns} 或 ${ins} )，以及两个可选的布尔类型参数 force 和 end
        const speakEvent = (ins, force = true, end = false) => {
            return new Promise((res, rej) => {
                ins.onerror = () => {
                    //遇到错误 结束 或者强制结束
                    if (end) {
                        endSpeak();
                    } else if (force) {
                        resetSpeakIcon();
                    }
                    res();
                }
                //在线语音
                if (existVoice >= 2) {
                    ins.onended = ins.onerror;
                    ins.play();
                } else {
                    //本地语音
                    ins.onend = ins.onerror;
                    speechSynthesis.speak(voiceIns);
                }
            })
        };
        //用于处理 WebSocket 实时语音转换的的数据存储和推送
        //它用于存储 Azure 实时语音转换返回的数据流
        let voiceData = [];
        //它用于建立与 Azure 实时语音转换服务的连接
        let voiceSocket;
        //用于记录当前是否正在将数据推送到音频源。
        let speechPushing = false;
        //然后是一个数组 speechQuene，它重写了 push 方法。在向 speechQuene 数组中添加数据时，
        //如果还未推送数据到音频源且 sourceBuffer 在更新，则将数据直接推送到音频源；否则，将其加入数组并缓存下来。
        let speechQuene = [];
        let sourceBuffer;
        //这里的 sourceBuffer 是一种新的技术，用于处理音频和视频的流数据，并将其附加到指定的元素中播放。
        //在这种情况下，它用于将来自 WebSocket 的数据流推送到音频源。
        speechQuene.push = function (buffer) {
            if (!speechPushing && !sourceBuffer.updating) {
                speechPushing = true;
                sourceBuffer.appendBuffer(buffer);
            } else {
                Array.prototype.push.call(this, buffer)
            }
        }
        //用于初始化 WebSocket 对象并建立连接。
        //如果 WebSocket 对象已经存在并正在连接中，则返回一个已在等待的 Promise，而不执行任何其他操作。
        const initSocket = () => {
            return new Promise((res, rej) => {
                if (!voiceSocket || voiceSocket.readyState > 1) {
                    voiceSocket = new WebSocket(existVoice === 3 ? getAzureWSURL() : edgeTTSURL);
                    voiceSocket.binaryType = "arraybuffer";
                    voiceSocket.onopen = () => {
                        res();
                    };
                    voiceSocket.onerror = () => {
                        rej();
                    }
                } else {
                    return res();
                }
            })
        }
        //用于初始化媒体资源和 WebSocket 连接，使其准备好接收来自实时语音转换服务的数据流。
        const initStreamVoice = (mediaSource) => {
            //该函数接收一个媒体资源对象 mediaSource
            return new Promise((r, j) => {
                //该函数接收一个媒体资源对象 mediaSource，并返回一个 Promise 对象。这个 Promise 中先执行了两个异步操作，它们都必须成功才能继续执行
                //这两个异步操作是：初始化 WebSocket 连接和监听媒体资源的 onsourceopen 事件
                Promise.all([initSocket(), new Promise(res => {
                    //mediaSource.onsourceopen 事件在媒体资源准备好接收音频数据时触发
                    mediaSource.onsourceopen = () => {
                        res();
                    };
                })]).then(() => {
                    r();
                })
            })
        }
        //这段代码定义了一些变量和函数，用于处理从 Azure 文字转语音服务下载音频文件的操作
        //变量是一个对象，用于存储文件下载队列。
        let downQuene = {};
        //是一个 WebSocket 对象，用于连接到 Azure 下载服务。
        let downSocket;
        //是一个函数，用于下载 Blob 对象，它接受两个参数：Blob 对象和下载的名称。
        const downBlob = (blob, name) => {
            let a = document.createElement("a");
            a.download = name;
            a.href = URL.createObjectURL(blob);
            a.click();
        }
        //这段代码定义了一个函数 initDownSocket，用于初始化 WebSocket 连接，以便从 Azure 服务下载已转换的音频文件
        const initDownSocket = () => {
            return new Promise((res, rej) => {
                //该函数返回一个 Promise 对象。如果 WebSocket 对象尚未创建或已关闭，则创建并连接它。
                //WebSocket 对象在与 Azure 服务成功建立连接后，返回解决的 Promise 对象；否则，返回一个拒绝的 Promise 对象。
                if (!downSocket || downSocket.readyState > 1) {
                    downSocket = new WebSocket(existVoice === 3 ? getAzureWSURL() : edgeTTSURL);
                    downSocket.binaryType = "arraybuffer";
                    downSocket.onopen = () => {
                        res();
                    };
                    //当从 WebSocket 对象接收到响应时，根据消息的类型以及其包含的数据，处理下载的逻辑。如果消息是 ArrayBuffer 类型，
                    //则该 ArrayBuffer 代表音频数据，需要缓存在下载队列 downQuene 中，直到 Azure 服务将所有数据传输完毕。
                    //如果消息包含 Path:turn.end，则下载结束，将所有缓存的数据转换为 Blob 对象并保存，同时调用 downBlob 函数进行下载
                    downSocket.onmessage = (e) => {
                        if (e.data instanceof ArrayBuffer) {
                            let text = new TextDecoder().decode(e.data.slice(0, 130));
                            let reqIdx = text.indexOf(":");
                            let uuid = text.slice(reqIdx + 1, reqIdx + 33);
                            downQuene[uuid]["blob"].push(e.data.slice(130));
                        } else if (e.data.indexOf("Path:turn.end") !== -1) {
                            let reqIdx = e.data.indexOf(":");
                            let uuid = e.data.slice(reqIdx + 1, reqIdx + 33);
                            let blob = new Blob(downQuene[uuid]["blob"], {
                                type: voiceMIME
                            });
                            let key = downQuene[uuid]["key"];
                            let name = downQuene[uuid]["name"];
                            voiceData[key] = blob;
                            downBlob(blob, name.slice(0, 16) + ".mp3");
                        }
                    }
                    downSocket.onerror = () => {
                        rej();
                    }
                } else {
                    return res();
                }
            })
        }
        //聊天框里面的上下文按钮下载音频
        const downloadAudio = async (idx) => {
            if (existVoice < 2) {
                return;
            }
            let type = data[idx].role === "user" ? 0 : 1;
            let voice = existVoice === 3 ? voiceRole[type].ShortName : voiceRole[type].Name;
            let volume = voiceVolume[type];
            let rate = voiceRate[type];
            let pitch = voicePitch[type];
            let style = azureStyle[type];
            let role = azureRole[type];
            let content = data[idx].content;
            let key = content + voice + volume + rate + pitch + (style ? style : "") + (role ? role : "");
            let blob = voiceData[key];
            if (blob) {
                downBlob(blob, content.slice(0, 16) + ".mp3");
            } else {
                await initDownSocket();
                let currDate = getTime();
                let lang = voiceRole[type].lang;
                let uuid = uuidv4();
                if (existVoice === 3) {
                    downSocket.send(getWSPre(currDate, uuid));
                }
                downSocket.send(getWSAudio(currDate, uuid));
                downSocket.send(getWSText(currDate, uuid, lang, voice, volume, rate, pitch, style, role,
                    content));
                downSocket["pending"] = true;
                downQuene[uuid] = {};
                downQuene[uuid]["name"] = content;
                downQuene[uuid]["key"] = key;
                downQuene[uuid]["blob"] = [];
            }
        }
        //如果不支持mse
        //NoMSEPending函数返回一个Promise对象。它监听websocket上的消息，如果收到的是ArrayBuffer类型的数据，则将其截取除了前130个字节以外的部分，
        //存储到bufArray数组中。如果收到的是字符串类型的数据，且包含“Path:turn.end”这个关键词，则表示此次语音数据传输结束，
        //将pending属性设置为false，将bufArray数组中存储的语音数据合并成一个Blob对象，并以指定的key值存储到voiceData对象中。
        //最后通过Promise的res方法将存储在voiceData对象中的Blob对象返回。
        const NoMSEPending = (key) => {
            return new Promise((res, rej) => {
                let bufArray = [];
                voiceSocket.onmessage = (e) => {
                    if (e.data instanceof ArrayBuffer) {
                        bufArray.push(e.data.slice(130));
                    } else if (e.data.indexOf("Path:turn.end") !== -1) {
                        voiceSocket["pending"] = false;
                        voiceData[key] = new Blob(bufArray, {
                            type: voiceMIME
                        });
                        res(voiceData[key]);
                    }
                }
            })
        }
        //
        const speechEvent = async (idx) => {
            //判断是否有要播放的音频数据，如果没有则直接返回
            if (!data[idx]) return;
            //结束正在播放的语音
            endSpeak();
            currentVoiceIdx = idx;
            //获取当前要播放的音频数据及相关配置信息（如音量、语速、声调等）
            if (!data[idx].content && enableContVoice) {
                if (currentVoiceIdx !== data.length - 1) {
                    return speechEvent(currentVoiceIdx + 1)
                } else {
                    return endSpeak()
                }
            };
            let type = data[idx].role === "user" ? 0 : 1;
            chatlog.children[systemRole ? idx - 1 : idx].children[1].lastChild.className = "pauseVoice";
            let content = data[idx].content;
            let volume = voiceVolume[type];
            let rate = voiceRate[type];
            let pitch = voicePitch[type];
            let style = azureStyle[type];
            let role = azureRole[type];
            //根据当前音量获取对应的语音类型（男声、女声等）
            if (existVoice >= 2) {
                if (!voiceIns) {
                    voiceIns = new Audio();
                }
                //定义一个音频对象 voiceIns，如果存在未播放的语音数据，则将其作为 voiceIns 的语音文本进行设置，
                //否则根据所选语音类型动态创建一个 SpeechSynthesisUtterance 对象，并将文本内容设置为当前要播放的内容。
                let voice = existVoice === 3 ? voiceRole[type].ShortName : voiceRole[type].Name;
                let key = content + voice + volume + rate + pitch + (style ? style : "") + (role ? role : "");
                let currData = voiceData[key];
                //如果存在正在播放的语音数据，则根据不同的语音合成技术选用相应的方式进行播放：若存在两个以上语音数据，则通过 WebSocket 传输语音数据来实现播放；
                //否则，直接使用 SpeechSynthesisUtterance 对象的 speak 方法进行播放。
                if (currData) {
                    voiceIns.src = URL.createObjectURL(currData);
                } else {
                    let mediaSource;
                    //支持MSE的直接使用控件播放 否则使用socket
                    if (supportMSE) {
                        mediaSource = new MediaSource;
                        voiceIns.src = URL.createObjectURL(mediaSource);
                        //初始化mse的数据流sourceBuffer
                        await initStreamVoice(mediaSource);
                        if (!sourceBuffer) {
                            sourceBuffer = mediaSource.addSourceBuffer(voiceMIME);
                        }
                        //数据流更新完成 则加入待播放的队列
                        sourceBuffer.onupdateend = function () {
                            speechPushing = false;
                            if (speechQuene.length) {
                                let buf = speechQuene.shift();
                                if (buf["end"]) {
                                    mediaSource.endOfStream();
                                } else {
                                    speechPushing = true;
                                    sourceBuffer.appendBuffer(buf);
                                }
                            }
                        };
                        let bufArray = [];
                        //解析Socket 推送过来的语音数据 并缓存到队列
                        voiceSocket.onmessage = (e) => {
                            if (e.data instanceof ArrayBuffer) {
                                let buf = e.data.slice(130);
                                bufArray.push(buf);
                                speechQuene.push(buf);
                            } else if (e.data.indexOf("Path:turn.end") !== -1) {
                                voiceSocket["pending"] = false;
                                voiceData[key] = new Blob(bufArray, {
                                    type: voiceMIME
                                });
                                if (!speechQuene.length && !speechPushing) {
                                    mediaSource.endOfStream();
                                } else {
                                    let buf = new ArrayBuffer();
                                    buf["end"] = true;
                                    speechQuene.push(buf);
                                }
                            }
                        }
                    } else {
                        await initSocket();
                    }
                    let currDate = getTime();
                    //语种
                    let lang = voiceRole[type].lang;
                    let uuid = uuidv4();
                    //如果当前语音类型为 Azure 语音服务且启用了传输语音预处理数据，则调用 getWSPre 函数发送一条 WebSocket 消息，
                    //用于告知语音服务开始准备预处理数据。
                    if (existVoice === 3) {
                        voiceSocket.send(getWSPre(currDate, uuid));
                    }
                    //调用 getWSAudio 函数发送一条 WebSocket 消息，用于告知语音服务开始传输语音数据。
                    voiceSocket.send(getWSAudio(currDate, uuid));
                    //调用 getWSText 函数发送一条 WebSocket 消息，包含了语音的具体信息（如内容、音量、语速、声调等参数）。
                    voiceSocket.send(getWSText(currDate, uuid, lang, voice, volume, rate, pitch, style, role,
                        content));
                    //设置 voiceSocket["pending"] 属性为 true，表示正在等待 WebSocket 响应
                    voiceSocket["pending"] = true;
                    //如果当前环境不支持 Media Source Extensions，则调用 NoMSEPending 函数等待语音数据返回，
                    //并通过 URL.createObjectURL 方法将得到的 Blob 数据转换为可播放的资源 URL，最终将其赋值给 voiceIns 的 src 属性用于播放
                    if (!supportMSE) {
                        let blob = await NoMSEPending(key);
                        voiceIns.src = URL.createObjectURL(blob);
                    }
                }
            } else {
                //创建 SpeechSynthesisUtterance 对象
                if (!voiceIns) {
                    voiceIns = new SpeechSynthesisUtterance();
                }
                voiceIns.voice = voiceRole[type]; //如男声、女声等。
                voiceIns.volume = volume; //设置语音音量，为一个介于0和1之间的数值
                voiceIns.rate = rate; //设置语音输出速度，为一个介于0.1和10之间的数值。1表示正常语速，小于1表示慢速，大于1表示快速
                voiceIns.pitch = pitch; //设置语音的音调，为一个介于0和2之间的数值。1表示正常音调，小于1表示低音调，大于1表示高音调
                voiceIns.text = content; //设置要转换为语音的文本内容
            }
            //调用硬件输出语音对象
            await speakEvent(voiceIns);
            //在语音播放完成后，如果开启了连续播放模式，则继续播放下一句音频数据，否则停止播放。
            if (enableContVoice) {
                if (currentVoiceIdx !== data.length - 1) {
                    return speechEvent(currentVoiceIdx + 1)
                } else {
                    endSpeak()
                }
            }
        };
        let autoVoiceSocket;
        let autoMediaSource;
        let voiceContentQuene = [];
        let voiceEndFlagQuene = [];
        let voiceBlobURLQuene = [];
        let autoOnlineVoiceFlag = false;
        //它会从文本内容数组中取出下一个内容并使用 WebSocket 发送该内容以完成语音合成。
        //同时，该函数会设置一些语音参数（例如，音量、语速、音调和音色）和语言。
        const autoAddQuene = () => {
            if (voiceContentQuene.length) {
                let content = voiceContentQuene.shift();
                let currDate = getTime();
                let uuid = uuidv4();
                let voice = voiceRole[1].Name;
                if (existVoice === 3) {
                    autoVoiceSocket.send(getWSPre(currDate, uuid));
                    voice = voiceRole[1].ShortName;
                }
                //将需要合成的文本发送出去
                autoVoiceSocket.send(getWSAudio(currDate, uuid));
                autoVoiceSocket.send(getWSText(currDate, uuid, voiceRole[1].lang, voice, voiceVolume[1], voiceRate[
                    1], voicePitch[1], azureStyle[1], azureRole[1], content));
                autoVoiceSocket["pending"] = true;
                //判断当前是否正在进行语音合成。
                autoOnlineVoiceFlag = true;
            }
        }
        //自动播放语音
        const autoSpeechEvent = (content, ele, force = false, end = false) => {
            if (ele.children[1].lastChild.className === "readyVoice") {
                ele.children[1].lastChild.className = "pauseVoice";
            }
            if (existVoice >= 2) {
                voiceContentQuene.push(content);
                voiceEndFlagQuene.push(end);
                if (!voiceIns) {
                    voiceIns = new Audio();
                }
                if (!autoVoiceSocket || autoVoiceSocket.readyState > 1) {
                    autoVoiceSocket = new WebSocket(existVoice === 3 ? getAzureWSURL() : edgeTTSURL);
                    autoVoiceSocket.binaryType = "arraybuffer";
                    autoVoiceSocket.onopen = () => {
                        autoAddQuene();
                    };

                    autoVoiceSocket.onerror = () => {
                        autoOnlineVoiceFlag = false;
                    };
                };
                let bufArray = [];
                autoVoiceSocket.onmessage = (e) => {
                    if (e.data instanceof ArrayBuffer) {
                        (supportMSE ? speechQuene : bufArray).push(e.data.slice(130));
                    } else {
                        if (e.data.indexOf("Path:turn.end") !== -1) {
                            autoVoiceSocket["pending"] = false;
                            autoOnlineVoiceFlag = false;
                            if (!supportMSE) {
                                let blob = new Blob(bufArray, {
                                    type: voiceMIME
                                });
                                bufArray = [];
                                if (blob.size) {
                                    let blobURL = URL.createObjectURL(blob);
                                    if (!voiceIns.src) {
                                        voiceIns.src = blobURL;
                                        voiceIns.play();
                                    } else {
                                        voiceBlobURLQuene.push(blobURL);
                                    }
                                }
                                autoAddQuene();
                            }
                            if (voiceEndFlagQuene.shift()) {
                                if (supportMSE) {
                                    if (!speechQuene.length && !speechPushing) {
                                        autoMediaSource.endOfStream();
                                    } else {
                                        let buf = new ArrayBuffer();
                                        buf["end"] = true;
                                        speechQuene.push(buf);
                                    }
                                } else {
                                    if (!voiceBlobURLQuene.length && !voiceIns.src) {
                                        endSpeak();
                                    } else {
                                        voiceBlobURLQuene.push("end");
                                    }
                                }
                            };
                            if (supportMSE) {
                                autoAddQuene();
                            }
                        }
                    }
                };
                if (!autoOnlineVoiceFlag && autoVoiceSocket.readyState) {
                    autoAddQuene();
                }
                if (supportMSE) {
                    if (!autoMediaSource) {
                        autoMediaSource = new MediaSource();
                        autoMediaSource.onsourceopen = () => {
                            if (!sourceBuffer) {
                                sourceBuffer = autoMediaSource.addSourceBuffer(voiceMIME);
                                sourceBuffer.onupdateend = () => {
                                    speechPushing = false;
                                    if (speechQuene.length) {
                                        let buf = speechQuene.shift();
                                        if (buf["end"]) {
                                            autoMediaSource.endOfStream();
                                        } else {
                                            speechPushing = true;
                                            sourceBuffer.appendBuffer(buf);
                                        }
                                    }
                                };
                            }
                        }
                    }
                    if (!voiceIns.src) {
                        voiceIns.src = URL.createObjectURL(autoMediaSource);
                        voiceIns.play();
                        voiceIns.onended = voiceIns.onerror = () => {
                            endSpeak();
                        }
                    }
                } else {
                    voiceIns.onended = voiceIns.onerror = () => {
                        if (voiceBlobURLQuene.length) {
                            let src = voiceBlobURLQuene.shift();
                            if (src === "end") {
                                endSpeak();
                            } else {
                                voiceIns.src = src;
                                voiceIns.currentTime = 0;
                                voiceIns.play();
                            }
                        } else {
                            voiceIns.currentTime = 0;
                            voiceIns.removeAttribute("src");
                        }
                    }
                }
            } else {
                voiceIns = new SpeechSynthesisUtterance(content);
                voiceIns.volume = voiceVolume[1];
                voiceIns.rate = voiceRate[1];
                voiceIns.pitch = voicePitch[1];
                voiceIns.voice = voiceRole[1];
                speakEvent(voiceIns, force, end);
            }
        };
        //确认对话框
        const confirmAction = (prompt) => {
            if (window.confirm(prompt)) {
                return true;
            } else {
                return false;
            }
        };
        let autoVoiceIdx = 0;
        let autoVoiceDataIdx;
        let controller;
        let controllerId;
        let refreshIdx;
        let currentResEle;
        let progressData = "";
        //获取openai 返回的数据流
        const streamGen = async (long) => {
            controller = new AbortController();
            controllerId = setTimeout(() => {
                notyf.error("请求超时，请稍后重试！");
                stopLoading();
            }, 30000);
            let headers = {
                "Content-Type": "application/json"
            };
            if (customAPIKey) {
                headers["Authorization"] = "Bearer " + customAPIKey;
            } else {
                headers["Authorization"] = "Bearer " + "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";
            }
            let isRefresh = refreshIdx !== void 0;
            if (isRefresh) {
                currentResEle = chatlog.children[systemRole ? refreshIdx - 1 : refreshIdx];
            } else if (!currentResEle) {
                currentResEle = createConvEle("response");
                currentResEle.children[0].innerHTML = "<br />";
                currentResEle.dataset.loading = true;
                scrollToBottom();
            }
            let idx = isRefresh ? refreshIdx : data.length;
            if (existVoice && enableAutoVoice && !long) {
                if (isRefresh) {
                    endSpeak();
                    autoVoiceDataIdx = currentVoiceIdx = idx;
                } else if (currentVoiceIdx !== data.length) {
                    endSpeak();
                    autoVoiceDataIdx = currentVoiceIdx = idx;
                }
            }
            let dataSlice;
            if (long) {
                idx = isRefresh ? refreshIdx : data.length - 1;
                dataSlice = [data[idx - 1], data[idx]];
                if (systemRole) {
                    dataSlice.unshift(data[0]);
                }
            } else if (enableCont) {
                dataSlice = data.slice(0, idx);
            } else {
                dataSlice = [data[idx - 1]];
                if (systemRole) {
                    dataSlice.unshift(data[0]);
                }
            }
            try {
                const res = await fetch(apiHost + API_URL, {
                    method: "POST",
                    headers,
                    body: JSON.stringify({
                        messages: dataSlice,
                        model: modelVersion,
                        stream: true,
                        temperature: roleTemp,
                        top_p: roleNature
                    }),
                    signal: controller.signal
                });
                clearTimeout(controllerId);
                controllerId = void 0;
                if (res.status !== 200) {
                    if (res.status === 401) {
                        notyf.error("API key错误或失效，请检查API key！")
                    } else if (res.status === 400) {
                        notyf.error("请求内容过大，请删除部分对话或打开设置关闭连续对话！");
                    } else if (res.status === 404) {
                        notyf.error("无权使用此模型，请打开设置选择其他GPT模型！");
                    } else if (res.status === 429) {
                        notyf.error(res.statusText ? "触发API调用频率限制，请稍后重试！" : "API使用超出限额，请检查您的账单！");
                    } else {
                        notyf.error("网关错误或超时，请稍后重试！");
                    }
                    stopLoading();
                    return;
                }
                const decoder = new TextDecoder();
                const reader = res.body.getReader();
                const readChunk = async () => {
                    return reader.read().then(async ({
                        value,
                        done
                    }) => {
                        if (!done) {
                            value = decoder.decode(value);
                            let chunks = value.split(/\n{2}/g);
                            chunks = chunks.filter(item => {
                                return item.trim();
                            });
                            for (let i = 0; i < chunks.length; i++) {
                                let chunk = chunks[i];
                                if (chunk) {
                                    let payload;
                                    try {
                                        payload = JSON.parse(chunk.slice(6));
                                    } catch (e) {
                                        break;
                                    }
                                    if (payload.choices[0].finish_reason) {
                                        let lenStop = payload.choices[0].finish_reason ===
                                            "length";
                                        let longReplyFlag = enableLongReply && lenStop;
                                        if (!enableLongReply && lenStop) {
                                            currentResEle.children[1].children[0]
                                                .className = "halfRefReq"
                                        } else {
                                            currentResEle.children[1].children[0]
                                                .className = "refreshReq"
                                        };
                                        if (existVoice && enableAutoVoice &&
                                            currentVoiceIdx === autoVoiceDataIdx) {
                                            let voiceText = longReplyFlag ? "" :
                                                progressData.slice(autoVoiceIdx),
                                                stop = !longReplyFlag;
                                            autoSpeechEvent(voiceText, currentResEle, false,
                                                stop);
                                        }
                                        break;
                                    } else {
                                        let content = payload.choices[0].delta.content;
                                        if (content) {
                                            if (!progressData && !content.trim()) continue;
                                            if (existVoice && enableAutoVoice &&
                                                currentVoiceIdx === autoVoiceDataIdx) {
                                                let spliter = content.match(
                                                    /\.|\?|!|。|？|！|\n/);
                                                if (spliter) {
                                                    let voiceText = progressData.slice(
                                                        autoVoiceIdx) + content.slice(0,
                                                        spliter.index + 1);
                                                    autoVoiceIdx += voiceText.length;
                                                    autoSpeechEvent(voiceText,
                                                        currentResEle);
                                                }
                                            }
                                            if (progressData) await delay();
                                            progressData += content;
                                            currentResEle.children[0].innerHTML = md.render(
                                                progressData);
                                            if (!isRefresh) {
                                                scrollToBottom();
                                            }
                                        }
                                    }
                                }
                            }
                            return readChunk();
                        } else {
                            if (isRefresh) {
                                data[refreshIdx].content = progressData;
                                if (longReplyFlag) return streamGen(true);
                            } else {
                                if (long) {
                                    data[data.length - 1].content = progressData
                                } else {
                                    data.push({
                                        role: "assistant",
                                        content: progressData
                                    })
                                }
                                if (longReplyFlag) return streamGen(true);
                            }
                            stopLoading(false);
                        }
                    });
                };
                await readChunk();
            } catch (e) {
                if (e.message.indexOf("aborted") === -1) {
                    notyf.error("访问接口失败，请检查接口！")
                    stopLoading();
                }
            }
        };
        //开始加载
        const loadAction = (bool) => {
            loading = bool;
            sendBtnEle.disabled = bool;
            sendBtnEle.className = bool ? " loading" : "loaded";
            stopEle.style.display = bool ? "flex" : "none";
            textInputEvent();
        }
        //停止加载
        const stopLoading = (abort = true) => {
            stopEle.style.display = "none";
            if (abort) {
                controller.abort();
                if (controllerId) clearTimeout(controllerId);
                if (delayId) clearTimeout(delayId);
                if (refreshIdx !== void 0) {
                    data[refreshIdx].content = progressData
                } else if (data[data.length - 1].role === "assistant") {
                    data[data.length - 1].content = progressData
                } else {
                    data.push({
                        role: "assistant",
                        content: progressData
                    })
                }
                if (existVoice && enableAutoVoice && currentVoiceIdx === autoVoiceDataIdx && progressData.length) {
                    let voiceText = progressData.slice(autoVoiceIdx);
                    autoSpeechEvent(voiceText, currentResEle, false, true);
                }
            }
            updateChats();
            controllerId = delayId = refreshIdx = void 0;
            autoVoiceIdx = 0;
            currentResEle.dataset.loading = false;
            currentResEle = null;
            progressData = "";
            loadAction(false);
        }
        //获取生成文本
        const generateText = async (message) => {
            loadAction(true);
            let requestEle = createConvEle("request");
            requestEle.children[0].textContent = message;
            data.push({
                role: "user",
                content: message
            });
            if (chatsData[activeChatIdx].name === "新的会话") {
                if (message.length > 50) {
                    message = message.slice(0, 47) + "...";
                }
                chatsData[activeChatIdx].name = message;
                chatListEle.children[activeChatIdx].children[1].textContent = message;
            }
            updateChats();
            scrollToBottom();
            await streamGen();
        };
        //回车的时候自动发送
        textarea.onkeydown = (e) => {
            if (e.keyCode === 13 && !e.shiftKey) {
                e.preventDefault();
                genFunc();
            }
        };
        //发送openai 对话的文本
        const genFunc = function () {
            // if (recing) {
            //     toggleRecEv();
            // }
            let message = textarea.value.trim();
            if (message.length !== 0) {
                if (loading === true) return;
                textarea.value = "";
                textarea.style.height = "47px";
                generateText(message);
            }
        };
        sendBtnEle.onclick = genFunc;
        stopEle.onclick = stopLoading;
        //点击清空对话的按钮
        document.getElementById("clearConv").onclick = () => {
            if (!loading && confirmAction("是否清空会话?")) {
                endSpeak();
                if (systemRole) {
                    data.length = 1
                } else {
                    data.length = 0
                }
                chatlog.innerHTML = "";
                updateChats();
            }
        }
    </script>
    <link href="//cdn.staticfile.org/github-markdown-css/5.2.0/github-markdown-light.min.css" rel="stylesheet">
    <link href="//cdn.staticfile.org/highlight.js/11.7.0/styles/github.min.css" rel="stylesheet">
    <link href="//cdn.staticfile.org/KaTeX/0.16.4/katex.min.css" rel="stylesheet">
    <link href="//npm.elemecdn.com/markdown-it-texmath@1.0.0/css/texmath.css" rel="stylesheet">
    <script defer>
        //这是一个使用 Fetch API 下载并读取 JSON 文件的 JavaScript 代码。首先创建了一个 AbortController 对象并设置了一个 10 秒的超时时间。
        //接下来使用 fetch() 函数获取指定 URL 的 JSON 文件，并通过该控制器的信号来控制请求超时。
        //在 fetch() 函数的回调函数中，首先将 JSON 对象的内容提取出来并转换为字符串，并将每个字符串存储在 presetRoleData 对象的键中。
        //同时，使用 DOM 操作，将 JSON 中每个对象的 act 属性创建一个 option 元素并作为 select 元素的一个子元素。
        //最后的 catch 语句则是对下载失败的处理
        const downRoleController = new AbortController();
        setTimeout(() => {
            downRoleController.abort();
        }, 10000);
        const preEle = document.getElementById("preSetSystem");
        fetch("https://cdn.jsdelivr.net/gh/PlexPt/awesome-ChatGPT-prompts-zh/prompts-zh.json", {
            signal: downRoleController.signal
        }).then(async (response) => {
            let res = await response.json();
            for (let i = 0; i < res.length; i++) {
                let key = "act" + i;
                presetRoleData[key] = res[i].prompt.trim();
                //给预设角色下拉选择框赋值
                let optionEle = document.createElement("option");
                optionEle.text = res[i].act;
                optionEle.value = key;
                preEle.options.add(optionEle);
            }
        }).catch(e => {})
    </script>
</body>

</html>